2
0

refactor: ♻️ Rename step to block

This commit is contained in:
Baptiste Arnaud
2022-06-11 07:27:38 +02:00
parent 8751766d0e
commit 2df8338505
297 changed files with 4292 additions and 3989 deletions

2
.gitignore vendored
View File

@ -22,3 +22,5 @@ firebaseServiceAccount.json
# Wordpress # Wordpress
.svn .svn
tags tags
dump.sql

View File

@ -1,25 +1,25 @@
import { Flex, HStack, StackProps, Text, Tooltip } from '@chakra-ui/react' import { Flex, HStack, StackProps, Text, Tooltip } from '@chakra-ui/react'
import { StepType, DraggableStepType } from 'models' import { BlockType, DraggableBlockType } from 'models'
import { useStepDnd } from 'contexts/GraphDndContext' import { useBlockDnd } from 'contexts/GraphDndContext'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { StepIcon } from './StepIcon' import { BlockIcon } from './BlockIcon'
import { StepTypeLabel } from './StepTypeLabel' import { BlockTypeLabel } from './BlockTypeLabel'
export const StepCard = ({ export const BlockCard = ({
type, type,
onMouseDown, onMouseDown,
isDisabled = false, isDisabled = false,
}: { }: {
type: DraggableStepType type: DraggableBlockType
isDisabled?: boolean isDisabled?: boolean
onMouseDown: (e: React.MouseEvent, type: DraggableStepType) => void onMouseDown: (e: React.MouseEvent, type: DraggableBlockType) => void
}) => { }) => {
const { draggedStepType } = useStepDnd() const { draggedBlockType } = useBlockDnd()
const [isMouseDown, setIsMouseDown] = useState(false) const [isMouseDown, setIsMouseDown] = useState(false)
useEffect(() => { useEffect(() => {
setIsMouseDown(draggedStepType === type) setIsMouseDown(draggedBlockType === type)
}, [draggedStepType, type]) }, [draggedBlockType, type])
const handleMouseDown = (e: React.MouseEvent) => onMouseDown(e, type) const handleMouseDown = (e: React.MouseEvent) => onMouseDown(e, type)
@ -43,8 +43,8 @@ export const StepCard = ({
> >
{!isMouseDown ? ( {!isMouseDown ? (
<> <>
<StepIcon type={type} /> <BlockIcon type={type} />
<StepTypeLabel type={type} /> <BlockTypeLabel type={type} />
</> </>
) : ( ) : (
<Text color="white" userSelect="none"> <Text color="white" userSelect="none">
@ -57,10 +57,10 @@ export const StepCard = ({
) )
} }
export const StepCardOverlay = ({ export const BlockCardOverlay = ({
type, type,
...props ...props
}: StackProps & { type: StepType }) => { }: StackProps & { type: BlockType }) => {
return ( return (
<HStack <HStack
borderWidth="1px" borderWidth="1px"
@ -76,8 +76,8 @@ export const StepCardOverlay = ({
zIndex={2} zIndex={2}
{...props} {...props}
> >
<StepIcon type={type} /> <BlockIcon type={type} />
<StepTypeLabel type={type} /> <BlockTypeLabel type={type} />
</HStack> </HStack>
) )
} }

View File

@ -30,67 +30,67 @@ import {
ZapierLogo, ZapierLogo,
} from 'assets/logos' } from 'assets/logos'
import { import {
BubbleStepType, BubbleBlockType,
InputStepType, InputBlockType,
IntegrationStepType, IntegrationBlockType,
LogicStepType, LogicBlockType,
StepType, BlockType,
} from 'models' } from 'models'
import React from 'react' import React from 'react'
type StepIconProps = { type: StepType } & IconProps type BlockIconProps = { type: BlockType } & IconProps
export const StepIcon = ({ type, ...props }: StepIconProps) => { export const BlockIcon = ({ type, ...props }: BlockIconProps) => {
switch (type) { switch (type) {
case BubbleStepType.TEXT: case BubbleBlockType.TEXT:
return <ChatIcon color="blue.500" {...props} /> return <ChatIcon color="blue.500" {...props} />
case BubbleStepType.IMAGE: case BubbleBlockType.IMAGE:
return <ImageIcon color="blue.500" {...props} /> return <ImageIcon color="blue.500" {...props} />
case BubbleStepType.VIDEO: case BubbleBlockType.VIDEO:
return <FilmIcon color="blue.500" {...props} /> return <FilmIcon color="blue.500" {...props} />
case BubbleStepType.EMBED: case BubbleBlockType.EMBED:
return <LayoutIcon color="blue.500" {...props} /> return <LayoutIcon color="blue.500" {...props} />
case InputStepType.TEXT: case InputBlockType.TEXT:
return <TextIcon color="orange.500" {...props} /> return <TextIcon color="orange.500" {...props} />
case InputStepType.NUMBER: case InputBlockType.NUMBER:
return <NumberIcon color="orange.500" {...props} /> return <NumberIcon color="orange.500" {...props} />
case InputStepType.EMAIL: case InputBlockType.EMAIL:
return <EmailIcon color="orange.500" {...props} /> return <EmailIcon color="orange.500" {...props} />
case InputStepType.URL: case InputBlockType.URL:
return <GlobeIcon color="orange.500" {...props} /> return <GlobeIcon color="orange.500" {...props} />
case InputStepType.DATE: case InputBlockType.DATE:
return <CalendarIcon color="orange.500" {...props} /> return <CalendarIcon color="orange.500" {...props} />
case InputStepType.PHONE: case InputBlockType.PHONE:
return <PhoneIcon color="orange.500" {...props} /> return <PhoneIcon color="orange.500" {...props} />
case InputStepType.CHOICE: case InputBlockType.CHOICE:
return <CheckSquareIcon color="orange.500" {...props} /> return <CheckSquareIcon color="orange.500" {...props} />
case InputStepType.PAYMENT: case InputBlockType.PAYMENT:
return <CreditCardIcon color="orange.500" {...props} /> return <CreditCardIcon color="orange.500" {...props} />
case InputStepType.RATING: case InputBlockType.RATING:
return <StarIcon color="orange.500" {...props} /> return <StarIcon color="orange.500" {...props} />
case LogicStepType.SET_VARIABLE: case LogicBlockType.SET_VARIABLE:
return <EditIcon color="purple.500" {...props} /> return <EditIcon color="purple.500" {...props} />
case LogicStepType.CONDITION: case LogicBlockType.CONDITION:
return <FilterIcon color="purple.500" {...props} /> return <FilterIcon color="purple.500" {...props} />
case LogicStepType.REDIRECT: case LogicBlockType.REDIRECT:
return <ExternalLinkIcon color="purple.500" {...props} /> return <ExternalLinkIcon color="purple.500" {...props} />
case LogicStepType.CODE: case LogicBlockType.CODE:
return <CodeIcon color="purple.500" {...props} /> return <CodeIcon color="purple.500" {...props} />
case LogicStepType.TYPEBOT_LINK: case LogicBlockType.TYPEBOT_LINK:
return <BoxIcon color="purple.500" {...props} /> return <BoxIcon color="purple.500" {...props} />
case IntegrationStepType.GOOGLE_SHEETS: case IntegrationBlockType.GOOGLE_SHEETS:
return <GoogleSheetsLogo {...props} /> return <GoogleSheetsLogo {...props} />
case IntegrationStepType.GOOGLE_ANALYTICS: case IntegrationBlockType.GOOGLE_ANALYTICS:
return <GoogleAnalyticsLogo {...props} /> return <GoogleAnalyticsLogo {...props} />
case IntegrationStepType.WEBHOOK: case IntegrationBlockType.WEBHOOK:
return <WebhookIcon {...props} /> return <WebhookIcon {...props} />
case IntegrationStepType.ZAPIER: case IntegrationBlockType.ZAPIER:
return <ZapierLogo {...props} /> return <ZapierLogo {...props} />
case IntegrationStepType.MAKE_COM: case IntegrationBlockType.MAKE_COM:
return <MakeComLogo {...props} /> return <MakeComLogo {...props} />
case IntegrationStepType.PABBLY_CONNECT: case IntegrationBlockType.PABBLY_CONNECT:
return <PabblyConnectLogo {...props} /> return <PabblyConnectLogo {...props} />
case IntegrationStepType.EMAIL: case IntegrationBlockType.EMAIL:
return <SendEmailIcon {...props} /> return <SendEmailIcon {...props} />
case 'start': case 'start':
return <FlagIcon {...props} /> return <FlagIcon {...props} />

View File

@ -1,87 +1,87 @@
import { Text, Tooltip } from '@chakra-ui/react' import { Text, Tooltip } from '@chakra-ui/react'
import { import {
BubbleStepType, BubbleBlockType,
InputStepType, InputBlockType,
IntegrationStepType, IntegrationBlockType,
LogicStepType, LogicBlockType,
StepType, BlockType,
} from 'models' } from 'models'
import React from 'react' import React from 'react'
type Props = { type: StepType } type Props = { type: BlockType }
export const StepTypeLabel = ({ type }: Props): JSX.Element => { export const BlockTypeLabel = ({ type }: Props): JSX.Element => {
switch (type) { switch (type) {
case 'start': case 'start':
return <Text>Start</Text> return <Text>Start</Text>
case BubbleStepType.TEXT: case BubbleBlockType.TEXT:
case InputStepType.TEXT: case InputBlockType.TEXT:
return <Text>Text</Text> return <Text>Text</Text>
case BubbleStepType.IMAGE: case BubbleBlockType.IMAGE:
return <Text>Image</Text> return <Text>Image</Text>
case BubbleStepType.VIDEO: case BubbleBlockType.VIDEO:
return <Text>Video</Text> return <Text>Video</Text>
case BubbleStepType.EMBED: case BubbleBlockType.EMBED:
return ( return (
<Tooltip label="Embed a pdf, an iframe, a website..."> <Tooltip label="Embed a pdf, an iframe, a website...">
<Text>Embed</Text> <Text>Embed</Text>
</Tooltip> </Tooltip>
) )
case InputStepType.NUMBER: case InputBlockType.NUMBER:
return <Text>Number</Text> return <Text>Number</Text>
case InputStepType.EMAIL: case InputBlockType.EMAIL:
return <Text>Email</Text> return <Text>Email</Text>
case InputStepType.URL: case InputBlockType.URL:
return <Text>Website</Text> return <Text>Website</Text>
case InputStepType.DATE: case InputBlockType.DATE:
return <Text>Date</Text> return <Text>Date</Text>
case InputStepType.PHONE: case InputBlockType.PHONE:
return <Text>Phone</Text> return <Text>Phone</Text>
case InputStepType.CHOICE: case InputBlockType.CHOICE:
return <Text>Button</Text> return <Text>Button</Text>
case InputStepType.PAYMENT: case InputBlockType.PAYMENT:
return <Text>Payment</Text> return <Text>Payment</Text>
case InputStepType.RATING: case InputBlockType.RATING:
return <Text>Rating</Text> return <Text>Rating</Text>
case LogicStepType.SET_VARIABLE: case LogicBlockType.SET_VARIABLE:
return <Text>Set variable</Text> return <Text>Set variable</Text>
case LogicStepType.CONDITION: case LogicBlockType.CONDITION:
return <Text>Condition</Text> return <Text>Condition</Text>
case LogicStepType.REDIRECT: case LogicBlockType.REDIRECT:
return <Text>Redirect</Text> return <Text>Redirect</Text>
case LogicStepType.CODE: case LogicBlockType.CODE:
return ( return (
<Tooltip label="Run Javascript code"> <Tooltip label="Run Javascript code">
<Text>Code</Text> <Text>Code</Text>
</Tooltip> </Tooltip>
) )
case LogicStepType.TYPEBOT_LINK: case LogicBlockType.TYPEBOT_LINK:
return ( return (
<Tooltip label="Link to another of your typebots"> <Tooltip label="Link to another of your typebots">
<Text>Typebot</Text> <Text>Typebot</Text>
</Tooltip> </Tooltip>
) )
case IntegrationStepType.GOOGLE_SHEETS: case IntegrationBlockType.GOOGLE_SHEETS:
return ( return (
<Tooltip label="Google Sheets"> <Tooltip label="Google Sheets">
<Text>Sheets</Text> <Text>Sheets</Text>
</Tooltip> </Tooltip>
) )
case IntegrationStepType.GOOGLE_ANALYTICS: case IntegrationBlockType.GOOGLE_ANALYTICS:
return ( return (
<Tooltip label="Google Analytics"> <Tooltip label="Google Analytics">
<Text>Analytics</Text> <Text>Analytics</Text>
</Tooltip> </Tooltip>
) )
case IntegrationStepType.WEBHOOK: case IntegrationBlockType.WEBHOOK:
return <Text>Webhook</Text> return <Text>Webhook</Text>
case IntegrationStepType.ZAPIER: case IntegrationBlockType.ZAPIER:
return <Text>Zapier</Text> return <Text>Zapier</Text>
case IntegrationStepType.MAKE_COM: case IntegrationBlockType.MAKE_COM:
return <Text>Make.com</Text> return <Text>Make.com</Text>
case IntegrationStepType.PABBLY_CONNECT: case IntegrationBlockType.PABBLY_CONNECT:
return <Text>Pabbly</Text> return <Text>Pabbly</Text>
case IntegrationStepType.EMAIL: case IntegrationBlockType.EMAIL:
return <Text>Email</Text> return <Text>Email</Text>
} }
} }

View File

@ -10,20 +10,20 @@ import {
Fade, Fade,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { import {
BubbleStepType, BubbleBlockType,
DraggableStepType, DraggableBlockType,
InputStepType, InputBlockType,
IntegrationStepType, IntegrationBlockType,
LogicStepType, LogicBlockType,
} from 'models' } from 'models'
import { useStepDnd } from 'contexts/GraphDndContext' import { useBlockDnd } from 'contexts/GraphDndContext'
import React, { useState } from 'react' import React, { useState } from 'react'
import { StepCard, StepCardOverlay } from './StepCard' import { BlockCard, BlockCardOverlay } from './BlockCard'
import { LockedIcon, UnlockedIcon } from 'assets/icons' import { LockedIcon, UnlockedIcon } from 'assets/icons'
import { headerHeight } from 'components/shared/TypebotHeader' import { headerHeight } from 'components/shared/TypebotHeader'
export const StepsSideBar = () => { export const BlocksSideBar = () => {
const { setDraggedStepType, draggedStepType } = useStepDnd() const { setDraggedBlockType, draggedBlockType } = useBlockDnd()
const [position, setPosition] = useState({ const [position, setPosition] = useState({
x: 0, x: 0,
y: 0, y: 0,
@ -33,7 +33,7 @@ export const StepsSideBar = () => {
const [isExtended, setIsExtended] = useState(true) const [isExtended, setIsExtended] = useState(true)
const handleMouseMove = (event: MouseEvent) => { const handleMouseMove = (event: MouseEvent) => {
if (!draggedStepType) return if (!draggedBlockType) return
const { clientX, clientY } = event const { clientX, clientY } = event
setPosition({ setPosition({
...position, ...position,
@ -43,19 +43,19 @@ export const StepsSideBar = () => {
} }
useEventListener('mousemove', handleMouseMove) useEventListener('mousemove', handleMouseMove)
const handleMouseDown = (e: React.MouseEvent, type: DraggableStepType) => { const handleMouseDown = (e: React.MouseEvent, type: DraggableBlockType) => {
const element = e.currentTarget as HTMLDivElement const element = e.currentTarget as HTMLDivElement
const rect = element.getBoundingClientRect() const rect = element.getBoundingClientRect()
setPosition({ x: rect.left, y: rect.top }) setPosition({ x: rect.left, y: rect.top })
const x = e.clientX - rect.left const x = e.clientX - rect.left
const y = e.clientY - rect.top const y = e.clientY - rect.top
setRelativeCoordinates({ x, y }) setRelativeCoordinates({ x, y })
setDraggedStepType(type) setDraggedBlockType(type)
} }
const handleMouseUp = () => { const handleMouseUp = () => {
if (!draggedStepType) return if (!draggedBlockType) return
setDraggedStepType(undefined) setDraggedBlockType(undefined)
setPosition({ setPosition({
x: 0, x: 0,
y: 0, y: 0,
@ -116,8 +116,8 @@ export const StepsSideBar = () => {
Bubbles Bubbles
</Text> </Text>
<SimpleGrid columns={2} spacing="3"> <SimpleGrid columns={2} spacing="3">
{Object.values(BubbleStepType).map((type) => ( {Object.values(BubbleBlockType).map((type) => (
<StepCard key={type} type={type} onMouseDown={handleMouseDown} /> <BlockCard key={type} type={type} onMouseDown={handleMouseDown} />
))} ))}
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>
@ -127,8 +127,8 @@ export const StepsSideBar = () => {
Inputs Inputs
</Text> </Text>
<SimpleGrid columns={2} spacing="3"> <SimpleGrid columns={2} spacing="3">
{Object.values(InputStepType).map((type) => ( {Object.values(InputBlockType).map((type) => (
<StepCard key={type} type={type} onMouseDown={handleMouseDown} /> <BlockCard key={type} type={type} onMouseDown={handleMouseDown} />
))} ))}
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>
@ -138,8 +138,8 @@ export const StepsSideBar = () => {
Logic Logic
</Text> </Text>
<SimpleGrid columns={2} spacing="3"> <SimpleGrid columns={2} spacing="3">
{Object.values(LogicStepType).map((type) => ( {Object.values(LogicBlockType).map((type) => (
<StepCard key={type} type={type} onMouseDown={handleMouseDown} /> <BlockCard key={type} type={type} onMouseDown={handleMouseDown} />
))} ))}
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>
@ -149,21 +149,21 @@ export const StepsSideBar = () => {
Integrations Integrations
</Text> </Text>
<SimpleGrid columns={2} spacing="3"> <SimpleGrid columns={2} spacing="3">
{Object.values(IntegrationStepType).map((type) => ( {Object.values(IntegrationBlockType).map((type) => (
<StepCard <BlockCard
key={type} key={type}
type={type} type={type}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
isDisabled={type === IntegrationStepType.MAKE_COM} isDisabled={type === IntegrationBlockType.MAKE_COM}
/> />
))} ))}
</SimpleGrid> </SimpleGrid>
</Stack> </Stack>
{draggedStepType && ( {draggedBlockType && (
<Portal> <Portal>
<StepCardOverlay <BlockCardOverlay
type={draggedStepType} type={draggedBlockType}
onMouseUp={handleMouseUp} onMouseUp={handleMouseUp}
pos="fixed" pos="fixed"
top="0" top="0"

View File

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

View File

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

View File

@ -21,7 +21,7 @@ import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
export const PreviewDrawer = () => { export const PreviewDrawer = () => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
const { setRightPanel, startPreviewAtBlock } = useEditor() const { setRightPanel, startPreviewAtGroup } = useEditor()
const { setPreviewingEdge } = useGraph() const { setPreviewingEdge } = useGraph()
const [isResizing, setIsResizing] = useState(false) const [isResizing, setIsResizing] = useState(false)
const [width, setWidth] = useState(500) const [width, setWidth] = useState(500)
@ -96,14 +96,14 @@ export const PreviewDrawer = () => {
borderRadius={'lg'} borderRadius={'lg'}
h="full" h="full"
w="full" w="full"
key={restartKey + (startPreviewAtBlock ?? '')} key={restartKey + (startPreviewAtGroup ?? '')}
pointerEvents={isResizing ? 'none' : 'auto'} pointerEvents={isResizing ? 'none' : 'auto'}
> >
<TypebotViewer <TypebotViewer
typebot={publicTypebot} typebot={publicTypebot}
onNewBlockVisible={setPreviewingEdge} onNewGroupVisible={setPreviewingEdge}
onNewLog={handleNewLog} onNewLog={handleNewLog}
startBlockId={startPreviewAtBlock} startGroupId={startPreviewAtGroup}
isPreview isPreview
/> />
</Flex> </Flex>

View File

@ -9,12 +9,12 @@ import {
FormLabel, FormLabel,
NumberInput, NumberInput,
NumberInputField, NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
Switch, Switch,
Text, Text,
Image, Image,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { ColorPicker } from 'components/theme/GeneralSettings/ColorPicker' import { ColorPicker } from 'components/theme/GeneralSettings/ColorPicker'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'

View File

@ -5,11 +5,10 @@ import {
Heading, Heading,
NumberInput, NumberInput,
NumberInputField, NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper,
Switch, Switch,
HStack, HStack,
NumberIncrementStepper,
NumberDecrementStepper,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { PopupParams } from 'typebot-js' import { PopupParams } from 'typebot-js'
@ -55,10 +54,10 @@ export const PopupEmbedSettings = ({
min={0} min={0}
> >
<NumberInputField /> <NumberInputField />
<NumberInputStepper> <NumberIncrementStepper>
<NumberIncrementStepper /> <NumberIncrementStepper />
<NumberDecrementStepper /> <NumberDecrementStepper />
</NumberInputStepper> </NumberIncrementStepper>
</NumberInput> </NumberInput>
)} )}
</Flex> </Flex>

View File

@ -17,22 +17,22 @@ export const DrawingEdge = () => {
connectingIds, connectingIds,
sourceEndpoints, sourceEndpoints,
targetEndpoints, targetEndpoints,
blocksCoordinates, groupsCoordinates,
} = useGraph() } = useGraph()
const { createEdge } = useTypebot() const { createEdge } = useTypebot()
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }) const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
const sourceBlockCoordinates = const sourceGroupCoordinates =
blocksCoordinates && blocksCoordinates[connectingIds?.source.blockId ?? ''] groupsCoordinates && groupsCoordinates[connectingIds?.source.groupId ?? '']
const targetBlockCoordinates = const targetGroupCoordinates =
blocksCoordinates && blocksCoordinates[connectingIds?.target?.blockId ?? ''] groupsCoordinates && groupsCoordinates[connectingIds?.target?.groupId ?? '']
const sourceTop = useMemo(() => { const sourceTop = useMemo(() => {
if (!connectingIds) return 0 if (!connectingIds) return 0
return getEndpointTopOffset({ return getEndpointTopOffset({
endpoints: sourceEndpoints, endpoints: sourceEndpoints,
graphOffsetY: graphPosition.y, graphOffsetY: graphPosition.y,
endpointId: connectingIds.source.itemId ?? connectingIds.source.stepId, endpointId: connectingIds.source.itemId ?? connectingIds.source.blockId,
graphScale: graphPosition.scale, graphScale: graphPosition.scale,
}) })
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -43,32 +43,32 @@ export const DrawingEdge = () => {
return getEndpointTopOffset({ return getEndpointTopOffset({
endpoints: targetEndpoints, endpoints: targetEndpoints,
graphOffsetY: graphPosition.y, graphOffsetY: graphPosition.y,
endpointId: connectingIds.target?.stepId, endpointId: connectingIds.target?.blockId,
graphScale: graphPosition.scale, graphScale: graphPosition.scale,
}) })
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [connectingIds, targetEndpoints]) }, [connectingIds, targetEndpoints])
const path = useMemo(() => { const path = useMemo(() => {
if (!sourceTop || !sourceBlockCoordinates) return `` if (!sourceTop || !sourceGroupCoordinates) return ``
return targetBlockCoordinates return targetGroupCoordinates
? computeConnectingEdgePath({ ? computeConnectingEdgePath({
sourceBlockCoordinates, sourceGroupCoordinates,
targetBlockCoordinates, targetGroupCoordinates,
sourceTop, sourceTop,
targetTop, targetTop,
graphScale: graphPosition.scale, graphScale: graphPosition.scale,
}) })
: computeEdgePathToMouse({ : computeEdgePathToMouse({
sourceBlockCoordinates, sourceGroupCoordinates,
mousePosition, mousePosition,
sourceTop, sourceTop,
}) })
}, [ }, [
sourceTop, sourceTop,
sourceBlockCoordinates, sourceGroupCoordinates,
targetBlockCoordinates, targetGroupCoordinates,
targetTop, targetTop,
mousePosition, mousePosition,
graphPosition.scale, graphPosition.scale,

View File

@ -13,37 +13,37 @@ import { isFreePlan } from 'services/workspace'
import { byId, isDefined } from 'utils' import { byId, isDefined } from 'utils'
type Props = { type Props = {
blockId: string groupId: string
answersCounts: AnswersCount[] answersCounts: AnswersCount[]
onUnlockProPlanClick?: () => void onUnlockProPlanClick?: () => void
} }
export const DropOffEdge = ({ export const DropOffEdge = ({
answersCounts, answersCounts,
blockId, groupId,
onUnlockProPlanClick, onUnlockProPlanClick,
}: Props) => { }: Props) => {
const { workspace } = useWorkspace() const { workspace } = useWorkspace()
const { sourceEndpoints, blocksCoordinates, graphPosition } = useGraph() const { sourceEndpoints, groupsCoordinates, graphPosition } = useGraph()
const { publishedTypebot } = useTypebot() const { publishedTypebot } = useTypebot()
const isUserOnFreePlan = isFreePlan(workspace) const isUserOnFreePlan = isFreePlan(workspace)
const totalAnswers = useMemo( const totalAnswers = useMemo(
() => answersCounts.find((a) => a.blockId === blockId)?.totalAnswers, () => answersCounts.find((a) => a.groupId === groupId)?.totalAnswers,
[answersCounts, blockId] [answersCounts, groupId]
) )
const { totalDroppedUser, dropOffRate } = useMemo(() => { const { totalDroppedUser, dropOffRate } = useMemo(() => {
if (!publishedTypebot || totalAnswers === undefined) if (!publishedTypebot || totalAnswers === undefined)
return { previousTotal: undefined, dropOffRate: undefined } return { previousTotal: undefined, dropOffRate: undefined }
const previousBlockIds = publishedTypebot.edges const previousGroupIds = publishedTypebot.edges
.map((edge) => .map((edge) =>
edge.to.blockId === blockId ? edge.from.blockId : undefined edge.to.groupId === groupId ? edge.from.groupId : undefined
) )
.filter(isDefined) .filter(isDefined)
const previousTotal = answersCounts const previousTotal = answersCounts
.filter((a) => previousBlockIds.includes(a.blockId)) .filter((a) => previousGroupIds.includes(a.groupId))
.reduce((prev, acc) => acc.totalAnswers + prev, 0) .reduce((prev, acc) => acc.totalAnswers + prev, 0)
if (previousTotal === 0) if (previousTotal === 0)
return { previousTotal: undefined, dropOffRate: undefined } return { previousTotal: undefined, dropOffRate: undefined }
@ -53,25 +53,25 @@ export const DropOffEdge = ({
totalDroppedUser, totalDroppedUser,
dropOffRate: Math.round((totalDroppedUser / previousTotal) * 100), dropOffRate: Math.round((totalDroppedUser / previousTotal) * 100),
} }
}, [answersCounts, blockId, totalAnswers, publishedTypebot]) }, [answersCounts, groupId, totalAnswers, publishedTypebot])
const block = publishedTypebot?.blocks.find(byId(blockId)) const group = publishedTypebot?.groups.find(byId(groupId))
const sourceTop = useMemo( const sourceTop = useMemo(
() => () =>
getEndpointTopOffset({ getEndpointTopOffset({
endpoints: sourceEndpoints, endpoints: sourceEndpoints,
graphOffsetY: graphPosition.y, graphOffsetY: graphPosition.y,
endpointId: block?.steps[block.steps.length - 1].id, endpointId: group?.blocks[group.blocks.length - 1].id,
graphScale: graphPosition.scale, graphScale: graphPosition.scale,
}), }),
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[block?.steps, sourceEndpoints, blocksCoordinates] [group?.blocks, sourceEndpoints, groupsCoordinates]
) )
const labelCoordinates = useMemo(() => { const labelCoordinates = useMemo(() => {
if (!blocksCoordinates[blockId]) return if (!groupsCoordinates[groupId]) return
return computeSourceCoordinates(blocksCoordinates[blockId], sourceTop ?? 0) return computeSourceCoordinates(groupsCoordinates[groupId], sourceTop ?? 0)
}, [blocksCoordinates, blockId, sourceTop]) }, [groupsCoordinates, groupId, sourceTop])
if (!labelCoordinates) return <></> if (!labelCoordinates) return <></>
return ( return (

View File

@ -25,7 +25,7 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
previewingEdge, previewingEdge,
sourceEndpoints, sourceEndpoints,
targetEndpoints, targetEndpoints,
blocksCoordinates, groupsCoordinates,
graphPosition, graphPosition,
isReadOnly, isReadOnly,
setPreviewingEdge, setPreviewingEdge,
@ -37,10 +37,10 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
const isPreviewing = isMouseOver || previewingEdge?.id === edge.id const isPreviewing = isMouseOver || previewingEdge?.id === edge.id
const sourceBlockCoordinates = const sourceGroupCoordinates =
blocksCoordinates && blocksCoordinates[edge.from.blockId] groupsCoordinates && groupsCoordinates[edge.from.groupId]
const targetBlockCoordinates = const targetGroupCoordinates =
blocksCoordinates && blocksCoordinates[edge.to.blockId] groupsCoordinates && groupsCoordinates[edge.to.groupId]
const sourceTop = useMemo( const sourceTop = useMemo(
() => () =>
@ -51,7 +51,7 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
graphScale: graphPosition.scale, graphScale: graphPosition.scale,
}), }),
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
[sourceBlockCoordinates?.y, edge, sourceEndpoints, refreshEdge] [sourceGroupCoordinates?.y, edge, sourceEndpoints, refreshEdge]
) )
useEffect(() => { useEffect(() => {
@ -62,7 +62,7 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
getEndpointTopOffset({ getEndpointTopOffset({
endpoints: targetEndpoints, endpoints: targetEndpoints,
graphOffsetY: graphPosition.y, graphOffsetY: graphPosition.y,
endpointId: edge?.to.stepId, endpointId: edge?.to.blockId,
graphScale: graphPosition.scale, graphScale: graphPosition.scale,
}) })
) )
@ -71,24 +71,24 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
getEndpointTopOffset({ getEndpointTopOffset({
endpoints: targetEndpoints, endpoints: targetEndpoints,
graphOffsetY: graphPosition.y, graphOffsetY: graphPosition.y,
endpointId: edge?.to.stepId, endpointId: edge?.to.blockId,
graphScale: graphPosition.scale, graphScale: graphPosition.scale,
}) })
) )
}, [ }, [
targetBlockCoordinates?.y, targetGroupCoordinates?.y,
targetEndpoints, targetEndpoints,
graphPosition.y, graphPosition.y,
edge?.to.stepId, edge?.to.blockId,
graphPosition.scale, graphPosition.scale,
]) ])
const path = useMemo(() => { const path = useMemo(() => {
if (!sourceBlockCoordinates || !targetBlockCoordinates || !sourceTop) if (!sourceGroupCoordinates || !targetGroupCoordinates || !sourceTop)
return `` return ``
const anchorsPosition = getAnchorsPosition({ const anchorsPosition = getAnchorsPosition({
sourceBlockCoordinates, sourceGroupCoordinates,
targetBlockCoordinates, targetGroupCoordinates,
sourceTop, sourceTop,
targetTop, targetTop,
graphScale: graphPosition.scale, graphScale: graphPosition.scale,
@ -96,10 +96,10 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
return computeEdgePath(anchorsPosition) return computeEdgePath(anchorsPosition)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [
sourceBlockCoordinates?.x, sourceGroupCoordinates?.x,
sourceBlockCoordinates?.y, sourceGroupCoordinates?.y,
targetBlockCoordinates?.x, targetGroupCoordinates?.x,
targetBlockCoordinates?.y, targetGroupCoordinates?.y,
sourceTop, sourceTop,
]) ])

View File

@ -34,9 +34,9 @@ export const Edges = ({
))} ))}
{answersCounts?.slice(1)?.map((answerCount) => ( {answersCounts?.slice(1)?.map((answerCount) => (
<DropOffEdge <DropOffEdge
key={answerCount.blockId} key={answerCount.groupId}
answersCounts={answersCounts} answersCounts={answersCounts}
blockId={answerCount.blockId} groupId={answerCount.groupId}
onUnlockProPlanClick={onUnlockProPlanClick} onUnlockProPlanClick={onUnlockProPlanClick}
/> />
))} ))}

View File

@ -13,7 +13,7 @@ export const SourceEndpoint = ({
const { const {
setConnectingIds, setConnectingIds,
addSourceEndpoint, addSourceEndpoint,
blocksCoordinates, groupsCoordinates,
previewingEdge, previewingEdge,
} = useGraph() } = useGraph()
const ref = useRef<HTMLDivElement | null>(null) const ref = useRef<HTMLDivElement | null>(null)
@ -24,18 +24,18 @@ export const SourceEndpoint = ({
} }
useEffect(() => { useEffect(() => {
if (ranOnce || !ref.current || Object.keys(blocksCoordinates).length === 0) if (ranOnce || !ref.current || Object.keys(groupsCoordinates).length === 0)
return return
const id = source.itemId ?? source.stepId const id = source.itemId ?? source.blockId
addSourceEndpoint({ addSourceEndpoint({
id, id,
ref, ref,
}) })
setRanOnce(true) setRanOnce(true)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ref.current, blocksCoordinates]) }, [ref.current, groupsCoordinates])
if (!blocksCoordinates) return <></> if (!groupsCoordinates) return <></>
return ( return (
<Flex <Flex
ref={ref} ref={ref}
@ -62,7 +62,7 @@ export const SourceEndpoint = ({
borderWidth="3.5px" borderWidth="3.5px"
shadow={`sm`} shadow={`sm`}
borderColor={ borderColor={
previewingEdge?.from.stepId === source.stepId && previewingEdge?.from.blockId === source.blockId &&
previewingEdge.from.itemId === source.itemId previewingEdge.from.itemId === source.itemId
? 'blue.300' ? 'blue.300'
: 'blue.200' : 'blue.200'

View File

@ -3,11 +3,11 @@ import { useGraph } from 'contexts/GraphContext'
import React, { useEffect, useRef } from 'react' import React, { useEffect, useRef } from 'react'
export const TargetEndpoint = ({ export const TargetEndpoint = ({
stepId, blockId,
isVisible, isVisible,
...props ...props
}: BoxProps & { }: BoxProps & {
stepId: string blockId: string
isVisible?: boolean isVisible?: boolean
}) => { }) => {
const { addTargetEndpoint } = useGraph() const { addTargetEndpoint } = useGraph()
@ -16,7 +16,7 @@ export const TargetEndpoint = ({
useEffect(() => { useEffect(() => {
if (!ref.current) return if (!ref.current) return
addTargetEndpoint({ addTargetEndpoint({
id: stepId, id: blockId,
ref, ref,
}) })
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -6,9 +6,9 @@ import {
graphPositionDefaultValue, graphPositionDefaultValue,
useGraph, useGraph,
} from 'contexts/GraphContext' } from 'contexts/GraphContext'
import { useStepDnd } from 'contexts/GraphDndContext' import { useBlockDnd } from 'contexts/GraphDndContext'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { DraggableStepType, PublicTypebot, Typebot } from 'models' import { DraggableBlockType, PublicTypebot, Typebot } from 'models'
import { AnswersCount } from 'services/analytics' import { AnswersCount } from 'services/analytics'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable' import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'
@ -21,7 +21,7 @@ import { ZoomButtons } from './ZoomButtons'
const maxScale = 1.5 const maxScale = 1.5
const minScale = 0.1 const minScale = 0.1
const zoomButtonsScaleStep = 0.2 const zoomButtonsScaleBlock = 0.2
export const Graph = ({ export const Graph = ({
typebot, typebot,
@ -34,20 +34,20 @@ export const Graph = ({
onUnlockProPlanClick?: () => void onUnlockProPlanClick?: () => void
} & FlexProps) => { } & FlexProps) => {
const { const {
draggedStepType, draggedBlockType,
setDraggedStepType, setDraggedBlockType,
draggedStep, draggedBlock,
setDraggedStep, setDraggedBlock,
draggedItem, draggedItem,
setDraggedItem, setDraggedItem,
} = useStepDnd() } = useBlockDnd()
const graphContainerRef = useRef<HTMLDivElement | null>(null) const graphContainerRef = useRef<HTMLDivElement | null>(null)
const editorContainerRef = useRef<HTMLDivElement | null>(null) const editorContainerRef = useRef<HTMLDivElement | null>(null)
const { createBlock } = useTypebot() const { createGroup } = useTypebot()
const { const {
setGraphPosition: setGlobalGraphPosition, setGraphPosition: setGlobalGraphPosition,
setOpenedStepId, setOpenedBlockId,
updateBlockCoordinates, updateGroupCoordinates,
setPreviewingEdge, setPreviewingEdge,
connectingIds, connectingIds,
} = useGraph() } = useGraph()
@ -100,22 +100,22 @@ export const Graph = ({
const handleMouseUp = (e: MouseEvent) => { const handleMouseUp = (e: MouseEvent) => {
if (!typebot) return if (!typebot) return
if (draggedItem) setDraggedItem(undefined) if (draggedItem) setDraggedItem(undefined)
if (!draggedStep && !draggedStepType) return if (!draggedBlock && !draggedBlockType) return
const coordinates = projectMouse( const coordinates = projectMouse(
{ x: e.clientX, y: e.clientY }, { x: e.clientX, y: e.clientY },
graphPosition graphPosition
) )
const id = cuid() const id = cuid()
updateBlockCoordinates(id, coordinates) updateGroupCoordinates(id, coordinates)
createBlock({ createGroup({
id, id,
...coordinates, ...coordinates,
step: draggedStep ?? (draggedStepType as DraggableStepType), block: draggedBlock ?? (draggedBlockType as DraggableBlockType),
indices: { blockIndex: typebot.blocks.length, stepIndex: 0 }, indices: { groupIndex: typebot.groups.length, blockIndex: 0 },
}) })
setDraggedStep(undefined) setDraggedBlock(undefined)
setDraggedStepType(undefined) setDraggedBlockType(undefined)
} }
const handleCaptureMouseDown = (e: MouseEvent) => { const handleCaptureMouseDown = (e: MouseEvent) => {
@ -124,7 +124,7 @@ export const Graph = ({
} }
const handleClick = () => { const handleClick = () => {
setOpenedStepId(undefined) setOpenedBlockId(undefined)
setPreviewingEdge(undefined) setPreviewingEdge(undefined)
} }
@ -137,7 +137,7 @@ export const Graph = ({
}) })
} }
const zoom = (delta = zoomButtonsScaleStep, mousePosition?: Coordinates) => { const zoom = (delta = zoomButtonsScaleBlock, mousePosition?: Coordinates) => {
const { x: mouseX, y } = mousePosition ?? { x: 0, y: 0 } const { x: mouseX, y } = mousePosition ?? { x: 0, y: 0 }
const mouseY = y - headerHeight const mouseY = y - headerHeight
let scale = graphPosition.scale + delta let scale = graphPosition.scale + delta
@ -181,8 +181,8 @@ export const Graph = ({
<DraggableCore onDrag={onDrag} enableUserSelectHack={false}> <DraggableCore onDrag={onDrag} enableUserSelectHack={false}>
<Flex ref={graphContainerRef} position="relative" {...props}> <Flex ref={graphContainerRef} position="relative" {...props}>
<ZoomButtons <ZoomButtons
onZoomIn={() => zoom(zoomButtonsScaleStep)} onZoomIn={() => zoom(zoomButtonsScaleBlock)}
onZoomOut={() => zoom(-zoomButtonsScaleStep)} onZoomOut={() => zoom(-zoomButtonsScaleBlock)}
/> />
<Flex <Flex
flex="1" flex="1"

View File

@ -1,9 +1,9 @@
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { Block } from 'models' import { Group } from 'models'
import React from 'react' import React from 'react'
import { AnswersCount } from 'services/analytics' import { AnswersCount } from 'services/analytics'
import { Edges } from './Edges' import { Edges } from './Edges'
import { BlockNode } from './Nodes/BlockNode' import { GroupNode } from './Nodes/GroupNode'
type Props = { type Props = {
answersCounts?: AnswersCount[] answersCounts?: AnswersCount[]
@ -18,8 +18,8 @@ const MyComponent = ({ answersCounts, onUnlockProPlanClick }: Props) => {
answersCounts={answersCounts} answersCounts={answersCounts}
onUnlockProPlanClick={onUnlockProPlanClick} onUnlockProPlanClick={onUnlockProPlanClick}
/> />
{typebot?.blocks.map((block, idx) => ( {typebot?.groups.map((group, idx) => (
<BlockNode block={block as Block} blockIndex={idx} key={block.id} /> <GroupNode group={group as Group} groupIndex={idx} key={group.id} />
))} ))}
</> </>
) )

View File

@ -1,193 +1,248 @@
import { import {
Editable, Flex,
EditableInput, HStack,
EditablePreview, Popover,
IconButton, PopoverTrigger,
Stack, useDisclosure,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { Block } from 'models' import {
BubbleBlock,
BubbleBlockContent,
ConditionBlock,
DraggableBlock,
Block,
BlockWithOptions,
TextBubbleContent,
TextBubbleBlock,
} from 'models'
import { useGraph } from 'contexts/GraphContext' import { useGraph } from 'contexts/GraphContext'
import { useStepDnd } from 'contexts/GraphDndContext' import { BlockIcon } from 'components/editor/BlocksSideBar/BlockIcon'
import { StepNodesList } from '../StepNode/StepNodesList' import { isBubbleBlock, isTextBubbleBlock } from 'utils'
import { isDefined, isNotDefined } from 'utils' import { BlockNodeContent } from './BlockNodeContent/BlockNodeContent'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { ContextMenu } from 'components/shared/ContextMenu' import { ContextMenu } from 'components/shared/ContextMenu'
import { SettingsPopoverContent } from './SettingsPopoverContent'
import { BlockNodeContextMenu } from './BlockNodeContextMenu' import { BlockNodeContextMenu } from './BlockNodeContextMenu'
import { useDebounce } from 'use-debounce' import { SourceEndpoint } from '../../Endpoints/SourceEndpoint'
import { hasDefaultConnector } from 'services/typebots'
import { useRouter } from 'next/router'
import { SettingsModal } from './SettingsPopoverContent/SettingsModal'
import { BlockSettings } from './SettingsPopoverContent/SettingsPopoverContent'
import { TextBubbleEditor } from './TextBubbleEditor'
import { TargetEndpoint } from '../../Endpoints'
import { MediaBubblePopoverContent } from './MediaBubblePopoverContent'
import { NodePosition, useDragDistance } from 'contexts/GraphDndContext'
import { setMultipleRefs } from 'services/utils' import { setMultipleRefs } from 'services/utils'
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'
import { PlayIcon } from 'assets/icons'
import { RightPanel, useEditor } from 'contexts/EditorContext'
type Props = { export const BlockNode = ({
block,
isConnectable,
indices,
onMouseDown,
}: {
block: Block block: Block
blockIndex: number isConnectable: boolean
} indices: { blockIndex: number; groupIndex: number }
onMouseDown?: (blockNodePosition: NodePosition, block: DraggableBlock) => void
export const BlockNode = ({ block, blockIndex }: Props) => { }) => {
const { query } = useRouter()
const { const {
connectingIds,
setConnectingIds, setConnectingIds,
connectingIds,
openedBlockId,
setOpenedBlockId,
setFocusedGroupId,
previewingEdge, previewingEdge,
blocksCoordinates,
updateBlockCoordinates,
isReadOnly,
focusedBlockId,
setFocusedBlockId,
graphPosition,
} = useGraph() } = useGraph()
const { typebot, updateBlock } = useTypebot() const { updateBlock } = useTypebot()
const { setMouseOverBlock, mouseOverBlock } = useStepDnd()
const [isMouseDown, setIsMouseDown] = useState(false)
const [isConnecting, setIsConnecting] = useState(false) const [isConnecting, setIsConnecting] = useState(false)
const { setRightPanel, setStartPreviewAtBlock } = useEditor() const [isPopoverOpened, setIsPopoverOpened] = useState(
const isPreviewing = openedBlockId === block.id
previewingEdge?.from.blockId === block.id || )
(previewingEdge?.to.blockId === block.id && const [isEditing, setIsEditing] = useState<boolean>(
isNotDefined(previewingEdge.to.stepId)) isTextBubbleBlock(block) && block.content.plainText === ''
const isStartBlock = )
isDefined(block.steps[0]) && block.steps[0].type === 'start'
const blockCoordinates = blocksCoordinates[block.id]
const blockRef = useRef<HTMLDivElement | null>(null) const blockRef = useRef<HTMLDivElement | null>(null)
const [debouncedBlockPosition] = useDebounce(blockCoordinates, 100)
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(() => { useEffect(() => {
if (!debouncedBlockPosition || isReadOnly) return if (query.blockId?.toString() === block.id) setOpenedBlockId(block.id)
if (
debouncedBlockPosition?.x === block.graphCoordinates.x &&
debouncedBlockPosition.y === block.graphCoordinates.y
)
return
updateBlock(blockIndex, { graphCoordinates: debouncedBlockPosition })
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedBlockPosition]) }, [query])
useEffect(() => { useEffect(() => {
setIsConnecting( setIsConnecting(
connectingIds?.target?.blockId === block.id && connectingIds?.target?.groupId === block.groupId &&
isNotDefined(connectingIds.target?.stepId) connectingIds?.target?.blockId === block.id
) )
}, [block.id, connectingIds]) }, [connectingIds, block.groupId, block.id])
const handleTitleSubmit = (title: string) => const handleModalClose = () => {
updateBlock(blockIndex, { title }) updateBlock(indices, { ...block })
onModalClose()
const handleMouseDown = (e: React.MouseEvent) => {
e.stopPropagation()
} }
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (isReadOnly) return
if (mouseOverBlock?.id !== block.id && !isStartBlock)
setMouseOverBlock({ id: block.id, ref: blockRef })
if (connectingIds) if (connectingIds)
setConnectingIds({ ...connectingIds, target: { blockId: block.id } }) setConnectingIds({
...connectingIds,
target: { groupId: block.groupId, blockId: block.id },
})
} }
const handleMouseLeave = () => { const handleMouseLeave = () => {
if (isReadOnly) return if (connectingIds?.target)
setMouseOverBlock(undefined) setConnectingIds({
if (connectingIds) setConnectingIds({ ...connectingIds, target: undefined }) ...connectingIds,
target: { ...connectingIds.target, blockId: undefined },
})
} }
const onDrag = (_: DraggableEvent, draggableData: DraggableData) => { const handleCloseEditor = (content: TextBubbleContent) => {
const { deltaX, deltaY } = draggableData const updatedBlock = { ...block, content } as Block
updateBlockCoordinates(block.id, { updateBlock(indices, updatedBlock)
x: blockCoordinates.x + deltaX / graphPosition.scale, setIsEditing(false)
y: blockCoordinates.y + deltaY / graphPosition.scale,
})
} }
const onDragStart = () => { const handleClick = (e: React.MouseEvent) => {
setFocusedBlockId(block.id) setFocusedGroupId(block.groupId)
setIsMouseDown(true) e.stopPropagation()
if (isTextBubbleBlock(block)) setIsEditing(true)
setOpenedBlockId(block.id)
} }
const startPreviewAtThisBlock = () => { const handleExpandClick = () => {
setStartPreviewAtBlock(block.id) setOpenedBlockId(undefined)
setRightPanel(RightPanel.PREVIEW) onModalOpen()
} }
const onDragStop = () => setIsMouseDown(false) const handleBlockUpdate = (updates: Partial<Block>) =>
return ( 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
initialValue={block.content.richText}
onClose={handleCloseEditor}
/>
) : (
<ContextMenu<HTMLDivElement> <ContextMenu<HTMLDivElement>
renderMenu={() => <BlockNodeContextMenu blockIndex={blockIndex} />} renderMenu={() => <BlockNodeContextMenu indices={indices} />}
isDisabled={isReadOnly || isStartBlock}
> >
{(ref, isOpened) => ( {(ref, isOpened) => (
<DraggableCore <Popover
enableUserSelectHack={false} placement="left"
onDrag={onDrag} isLazy
onStart={onDragStart} isOpen={isPopoverOpened}
onStop={onDragStop} closeOnBlur={false}
onMouseDown={(e) => e.stopPropagation()}
> >
<Stack <PopoverTrigger>
ref={setMultipleRefs([ref, blockRef])} <Flex
data-testid="block" pos="relative"
p="4" ref={setMultipleRefs([ref, blockRef])}
rounded="xl" onMouseEnter={handleMouseEnter}
bgColor="#ffffff" onMouseLeave={handleMouseLeave}
borderWidth="2px" onClick={handleClick}
borderColor={ data-testid={`block`}
isConnecting || isOpened || isPreviewing ? 'blue.400' : '#ffffff' w="full"
}
w="300px"
transition="border 300ms, box-shadow 200ms"
pos="absolute"
style={{
transform: `translate(${blockCoordinates?.x ?? 0}px, ${
blockCoordinates?.y ?? 0
}px)`,
}}
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
cursor={isMouseDown ? 'grabbing' : 'pointer'}
shadow="md"
_hover={{ shadow: 'lg' }}
zIndex={focusedBlockId === block.id ? 10 : 1}
>
<Editable
defaultValue={block.title}
onSubmit={handleTitleSubmit}
fontWeight="semibold"
pointerEvents={isReadOnly || isStartBlock ? 'none' : 'auto'}
> >
<EditablePreview <HStack
_hover={{ bgColor: 'gray.200' }} flex="1"
px="1" userSelect="none"
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}
/> />
<EditableInput <SettingsModal isOpen={isModalOpen} onClose={handleModalClose}>
minW="0" <BlockSettings
px="1" block={block}
onMouseDown={(e) => e.stopPropagation()} onBlockChange={handleBlockUpdate}
/> />
</Editable> </SettingsModal>
{typebot && ( </>
<StepNodesList )}
blockId={block.id} {isMediaBubbleBlock(block) && (
steps={block.steps} <MediaBubblePopoverContent
blockIndex={blockIndex} block={block}
blockRef={ref} onContentChange={handleContentChange}
isStartBlock={isStartBlock}
/>
)}
<IconButton
icon={<PlayIcon />}
aria-label={'Preview bot from this group'}
pos="absolute"
right={2}
top={0}
size="sm"
variant="outline"
onClick={startPreviewAtThisBlock}
/> />
</Stack> )}
</DraggableCore> </Popover>
)} )}
</ContextMenu> </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,156 @@
import { Text } from '@chakra-ui/react'
import {
Block,
StartBlock,
BubbleBlockType,
InputBlockType,
LogicBlockType,
IntegrationBlockType,
BlockIndices,
} from 'models'
import { isChoiceInput, isInputBlock } from 'utils'
import { ItemNodesList } from '../../ItemNode'
import {
EmbedBubbleContent,
SetVariableContent,
TextBubbleContent,
VideoBubbleContent,
WebhookContent,
WithVariableContent,
} from './contents'
import { ConfigureContent } from './contents/ConfigureContent'
import { ImageBubbleContent } from './contents/ImageBubbleContent'
import { PaymentInputContent } from './contents/PaymentInputContent'
import { PlaceholderContent } from './contents/PlaceholderContent'
import { RatingInputContent } from './contents/RatingInputContent'
import { SendEmailContent } from './contents/SendEmailContent'
import { TypebotLinkContent } from './contents/TypebotLinkContent'
import { ProviderWebhookContent } from './contents/ZapierContent'
type Props = {
block: Block | StartBlock
indices: BlockIndices
}
export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
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 (
<PlaceholderContent
placeholder={block.options.labels.placeholder}
isLong={block.options.isLong}
/>
)
}
case InputBlockType.NUMBER:
case InputBlockType.EMAIL:
case InputBlockType.URL:
case InputBlockType.PHONE: {
return (
<PlaceholderContent placeholder={block.options.labels.placeholder} />
)
}
case InputBlockType.DATE: {
return <Text color={'gray.500'}>Pick a date...</Text>
}
case InputBlockType.CHOICE: {
return <ItemNodesList block={block} indices={indices} />
}
case InputBlockType.PAYMENT: {
return <PaymentInputContent block={block} />
}
case InputBlockType.RATING: {
return <RatingInputContent block={block} />
}
case LogicBlockType.SET_VARIABLE: {
return <SetVariableContent block={block} />
}
case LogicBlockType.CONDITION: {
return <ItemNodesList block={block} indices={indices} isReadOnly />
}
case LogicBlockType.REDIRECT: {
return (
<ConfigureContent
label={
block.options?.url ? `Redirect to ${block.options?.url}` : undefined
}
/>
)
}
case LogicBlockType.CODE: {
return (
<ConfigureContent
label={
block.options?.content ? `Run ${block.options?.name}` : undefined
}
/>
)
}
case LogicBlockType.TYPEBOT_LINK:
return <TypebotLinkContent block={block} />
case IntegrationBlockType.GOOGLE_SHEETS: {
return (
<ConfigureContent
label={
block.options && 'action' in block.options
? block.options.action
: undefined
}
/>
)
}
case IntegrationBlockType.GOOGLE_ANALYTICS: {
return (
<ConfigureContent
label={
block.options?.action
? `Track "${block.options?.action}" `
: undefined
}
/>
)
}
case IntegrationBlockType.WEBHOOK: {
return <WebhookContent block={block} />
}
case IntegrationBlockType.ZAPIER: {
return (
<ProviderWebhookContent block={block} configuredLabel="Trigger zap" />
)
}
case IntegrationBlockType.PABBLY_CONNECT:
case IntegrationBlockType.MAKE_COM: {
return (
<ProviderWebhookContent
block={block}
configuredLabel="Trigger scenario"
/>
)
}
case IntegrationBlockType.EMAIL: {
return <SendEmailContent block={block} />
}
case 'start': {
return <Text>Start</Text>
}
}
}

View File

@ -1,13 +1,13 @@
import { Box, Text } from '@chakra-ui/react' import { Box, Text } from '@chakra-ui/react'
import { EmbedBubbleStep } from 'models' import { EmbedBubbleBlock } from 'models'
export const EmbedBubbleContent = ({ step }: { step: EmbedBubbleStep }) => { export const EmbedBubbleContent = ({ block }: { block: EmbedBubbleBlock }) => {
if (!step.content?.url) return <Text color="gray.500">Click to edit...</Text> if (!block.content?.url) return <Text color="gray.500">Click to edit...</Text>
return ( return (
<Box w="full" h="120px" pos="relative"> <Box w="full" h="120px" pos="relative">
<iframe <iframe
id="embed-bubble-content" id="embed-bubble-content"
src={step.content.url} src={block.content.url}
style={{ style={{
width: '100%', width: '100%',
height: '100%', height: '100%',

View File

@ -0,0 +1,21 @@
import { Box, Text, Image } from '@chakra-ui/react'
import { ImageBubbleBlock } from 'models'
export const ImageBubbleContent = ({ block }: { block: ImageBubbleBlock }) => {
const containsVariables =
block.content?.url?.includes('{{') && block.content.url.includes('}}')
return !block.content?.url ? (
<Text color={'gray.500'}>Click to edit...</Text>
) : (
<Box w="full">
<Image
src={
containsVariables ? '/images/dynamic-image.png' : block.content?.url
}
alt="Group image"
rounded="md"
objectFit="cover"
/>
</Box>
)
}

View File

@ -0,0 +1,20 @@
import { Text } from '@chakra-ui/react'
import { PaymentInputBlock } from 'models'
type Props = {
block: PaymentInputBlock
}
export const PaymentInputContent = ({ block }: Props) => {
if (
!block.options.amount ||
!block.options.credentialsId ||
!block.options.currency
)
return <Text color="gray.500">Configure...</Text>
return (
<Text noOfLines={0} pr="6">
Collect {block.options.amount} {block.options.currency}
</Text>
)
}

View File

@ -0,0 +1,12 @@
import { Text } from '@chakra-ui/react'
import { RatingInputBlock } from 'models'
type Props = {
block: RatingInputBlock
}
export const RatingInputContent = ({ block }: Props) => (
<Text noOfLines={0} pr="6">
Rate from 1 to {block.options.length}
</Text>
)

View File

@ -1,19 +1,19 @@
import { Tag, Text, Wrap, WrapItem } from '@chakra-ui/react' import { Tag, Text, Wrap, WrapItem } from '@chakra-ui/react'
import { SendEmailStep } from 'models' import { SendEmailBlock } from 'models'
type Props = { type Props = {
step: SendEmailStep block: SendEmailBlock
} }
export const SendEmailContent = ({ step }: Props) => { export const SendEmailContent = ({ block }: Props) => {
if (step.options.recipients.length === 0) if (block.options.recipients.length === 0)
return <Text color="gray.500">Configure...</Text> return <Text color="gray.500">Configure...</Text>
return ( return (
<Wrap noOfLines={2} pr="6"> <Wrap noOfLines={2} pr="6">
<WrapItem> <WrapItem>
<Text>Send email to</Text> <Text>Send email to</Text>
</WrapItem> </WrapItem>
{step.options.recipients.map((to) => ( {block.options.recipients.map((to) => (
<WrapItem key={to}> <WrapItem key={to}>
<Tag>{to}</Tag> <Tag>{to}</Tag>
</WrapItem> </WrapItem>

View File

@ -1,13 +1,13 @@
import { Text } from '@chakra-ui/react' import { Text } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { SetVariableStep } from 'models' import { SetVariableBlock } from 'models'
import { byId } from 'utils' import { byId } from 'utils'
export const SetVariableContent = ({ step }: { step: SetVariableStep }) => { export const SetVariableContent = ({ block }: { block: SetVariableBlock }) => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
const variableName = const variableName =
typebot?.variables.find(byId(step.options.variableId))?.name ?? '' typebot?.variables.find(byId(block.options.variableId))?.name ?? ''
const expression = step.options.expressionToEvaluate ?? '' const expression = block.options.expressionToEvaluate ?? ''
return ( return (
<Text color={'gray.500'} noOfLines={2}> <Text color={'gray.500'} noOfLines={2}>
{variableName === '' && expression === '' {variableName === '' && expression === ''

View File

@ -1,27 +1,27 @@
import { Flex } from '@chakra-ui/react' import { Flex } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { TextBubbleStep } from 'models' import { TextBubbleBlock } from 'models'
import React from 'react' import React from 'react'
import { parseVariableHighlight } from 'services/utils' import { parseVariableHighlight } from 'services/utils'
type Props = { type Props = {
step: TextBubbleStep block: TextBubbleBlock
} }
export const TextBubbleContent = ({ step }: Props) => { export const TextBubbleContent = ({ block }: Props) => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
if (!typebot) return <></> if (!typebot) return <></>
return ( return (
<Flex <Flex
w="90%" w="90%"
flexDir={'column'} flexDir={'column'}
opacity={step.content.html === '' ? '0.5' : '1'} opacity={block.content.html === '' ? '0.5' : '1'}
className="slate-html-container" className="slate-html-container"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: __html:
step.content.html === '' block.content.html === ''
? `<p>Click to edit...</p>` ? `<p>Click to edit...</p>`
: parseVariableHighlight(step.content.html, typebot), : parseVariableHighlight(block.content.html, typebot),
}} }}
/> />
) )

View File

@ -1,26 +1,27 @@
import { TypebotLinkStep } from 'models' import { TypebotLinkBlock } from 'models'
import React from 'react' import React from 'react'
import { Tag, Text } from '@chakra-ui/react' import { Tag, Text } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { byId } from 'utils' import { byId } from 'utils'
type Props = { type Props = {
step: TypebotLinkStep block: TypebotLinkBlock
} }
export const TypebotLinkContent = ({ step }: Props) => { export const TypebotLinkContent = ({ block }: Props) => {
const { linkedTypebots, typebot } = useTypebot() const { linkedTypebots, typebot } = useTypebot()
const isCurrentTypebot = const isCurrentTypebot =
typebot && typebot &&
(step.options.typebotId === typebot.id || (block.options.typebotId === typebot.id ||
step.options.typebotId === 'current') block.options.typebotId === 'current')
const linkedTypebot = isCurrentTypebot const linkedTypebot = isCurrentTypebot
? typebot ? typebot
: linkedTypebots?.find(byId(step.options.typebotId)) : linkedTypebots?.find(byId(block.options.typebotId))
const blockTitle = linkedTypebot?.blocks.find( const blockTitle = linkedTypebot?.groups.find(
byId(step.options.blockId) byId(block.options.groupId)
)?.title )?.title
if (!step.options.typebotId) return <Text color="gray.500">Configure...</Text> if (!block.options.typebotId)
return <Text color="gray.500">Configure...</Text>
return ( return (
<Text> <Text>
Jump{' '} Jump{' '}

View File

@ -1,15 +1,15 @@
import { Box, Text } from '@chakra-ui/react' import { Box, Text } from '@chakra-ui/react'
import { VideoBubbleStep, VideoBubbleContentType } from 'models' import { VideoBubbleBlock, VideoBubbleContentType } from 'models'
export const VideoBubbleContent = ({ step }: { step: VideoBubbleStep }) => { export const VideoBubbleContent = ({ block }: { block: VideoBubbleBlock }) => {
if (!step.content?.url || !step.content.type) if (!block.content?.url || !block.content.type)
return <Text color="gray.500">Click to edit...</Text> return <Text color="gray.500">Click to edit...</Text>
switch (step.content.type) { switch (block.content.type) {
case VideoBubbleContentType.URL: case VideoBubbleContentType.URL:
return ( return (
<Box w="full" h="120px" pos="relative"> <Box w="full" h="120px" pos="relative">
<video <video
key={step.content.url} key={block.content.url}
controls controls
style={{ style={{
width: '100%', width: '100%',
@ -20,20 +20,20 @@ export const VideoBubbleContent = ({ step }: { step: VideoBubbleStep }) => {
borderRadius: '10px', borderRadius: '10px',
}} }}
> >
<source src={step.content.url} /> <source src={block.content.url} />
</video> </video>
</Box> </Box>
) )
case VideoBubbleContentType.VIMEO: case VideoBubbleContentType.VIMEO:
case VideoBubbleContentType.YOUTUBE: { case VideoBubbleContentType.YOUTUBE: {
const baseUrl = const baseUrl =
step.content.type === VideoBubbleContentType.VIMEO block.content.type === VideoBubbleContentType.VIMEO
? 'https://player.vimeo.com/video' ? 'https://player.vimeo.com/video'
: 'https://www.youtube.com/embed' : 'https://www.youtube.com/embed'
return ( return (
<Box w="full" h="120px" pos="relative"> <Box w="full" h="120px" pos="relative">
<iframe <iframe
src={`${baseUrl}/${step.content.id}`} src={`${baseUrl}/${block.content.id}`}
allowFullScreen allowFullScreen
style={{ style={{
width: '100%', width: '100%',

View File

@ -1,13 +1,13 @@
import { Text } from '@chakra-ui/react' import { Text } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { WebhookStep } from 'models' import { WebhookBlock } from 'models'
import { byId } from 'utils' import { byId } from 'utils'
type Props = { type Props = {
step: WebhookStep block: WebhookBlock
} }
export const WebhookContent = ({ step: { webhookId } }: Props) => { export const WebhookContent = ({ block: { webhookId } }: Props) => {
const { webhooks } = useTypebot() const { webhooks } = useTypebot()
const webhook = webhooks.find(byId(webhookId)) const webhook = webhooks.find(byId(webhookId))

View File

@ -1,17 +1,17 @@
import { InputStep } from 'models' import { InputBlock } from 'models'
import { chakra, Text } from '@chakra-ui/react' import { chakra, Text } from '@chakra-ui/react'
import React from 'react' import React from 'react'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { byId } from 'utils' import { byId } from 'utils'
type Props = { type Props = {
step: InputStep block: InputBlock
} }
export const WithVariableContent = ({ step }: Props) => { export const WithVariableContent = ({ block }: Props) => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
const variableName = typebot?.variables.find( const variableName = typebot?.variables.find(
byId(step.options.variableId) byId(block.options.variableId)
)?.name )?.name
return ( return (

View File

@ -2,27 +2,27 @@ import { Text } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { import {
defaultWebhookAttributes, defaultWebhookAttributes,
MakeComStep, MakeComBlock,
PabblyConnectStep, PabblyConnectBlock,
Webhook, Webhook,
ZapierStep, ZapierBlock,
} from 'models' } from 'models'
import { useEffect } from 'react' import { useEffect } from 'react'
import { byId, isNotDefined } from 'utils' import { byId, isNotDefined } from 'utils'
type Props = { type Props = {
step: ZapierStep | MakeComStep | PabblyConnectStep block: ZapierBlock | MakeComBlock | PabblyConnectBlock
configuredLabel: string configuredLabel: string
} }
export const ProviderWebhookContent = ({ step, configuredLabel }: Props) => { export const ProviderWebhookContent = ({ block, configuredLabel }: Props) => {
const { webhooks, typebot, updateWebhook } = useTypebot() const { webhooks, typebot, updateWebhook } = useTypebot()
const webhook = webhooks.find(byId(step.webhookId)) const webhook = webhooks.find(byId(block.webhookId))
useEffect(() => { useEffect(() => {
if (!typebot) return if (!typebot) return
if (!webhook) { if (!webhook) {
const { webhookId } = step const { webhookId } = block
const newWebhook = { const newWebhook = {
id: webhookId, id: webhookId,
...defaultWebhookAttributes, ...defaultWebhookAttributes,

View File

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

View File

@ -1,17 +1,15 @@
import { MenuList, MenuItem } from '@chakra-ui/react' import { MenuList, MenuItem } from '@chakra-ui/react'
import { CopyIcon, TrashIcon } from 'assets/icons' import { CopyIcon, TrashIcon } from 'assets/icons'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { BlockIndices } from 'models'
export const BlockNodeContextMenu = ({ type Props = { indices: BlockIndices }
blockIndex, export const BlockNodeContextMenu = ({ indices }: Props) => {
}: {
blockIndex: number
}) => {
const { deleteBlock, duplicateBlock } = useTypebot() const { deleteBlock, duplicateBlock } = useTypebot()
const handleDeleteClick = () => deleteBlock(blockIndex) const handleDuplicateClick = () => duplicateBlock(indices)
const handleDuplicateClick = () => duplicateBlock(blockIndex) const handleDeleteClick = () => deleteBlock(indices)
return ( return (
<MenuList> <MenuList>

View File

@ -0,0 +1,27 @@
import { StackProps, HStack } from '@chakra-ui/react'
import { StartBlock, Block, BlockIndices } from 'models'
import { BlockIcon } from 'components/editor/BlocksSideBar/BlockIcon'
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

@ -1,37 +1,37 @@
import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react' import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react'
import { DraggableStep, DraggableStepType, Step } from 'models' import { DraggableBlock, DraggableBlockType, Block } from 'models'
import { import {
computeNearestPlaceholderIndex, computeNearestPlaceholderIndex,
useStepDnd, useBlockDnd,
} from 'contexts/GraphDndContext' } from 'contexts/GraphDndContext'
import { Coordinates, useGraph } from 'contexts/GraphContext' import { Coordinates, useGraph } from 'contexts/GraphContext'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { StepNode } from './StepNode' import { BlockNode } from './BlockNode'
import { StepNodeOverlay } from './StepNodeOverlay' import { BlockNodeOverlay } from './BlockNodeOverlay'
type Props = { type Props = {
blockId: string groupId: string
steps: Step[] blocks: Block[]
blockIndex: number groupIndex: number
blockRef: React.MutableRefObject<HTMLDivElement | null> groupRef: React.MutableRefObject<HTMLDivElement | null>
isStartBlock: boolean isStartGroup: boolean
} }
export const StepNodesList = ({ export const BlockNodesList = ({
blockId, groupId,
steps, blocks,
blockIndex, groupIndex,
blockRef, groupRef,
isStartBlock, isStartGroup,
}: Props) => { }: Props) => {
const { const {
draggedStep, draggedBlock,
setDraggedStep, setDraggedBlock,
draggedStepType, draggedBlockType,
mouseOverBlock, mouseOverGroup,
setDraggedStepType, setDraggedBlockType,
} = useStepDnd() } = useBlockDnd()
const { typebot, createStep, detachStepFromBlock } = useTypebot() const { typebot, createBlock, detachBlockFromGroup } = useTypebot()
const { isReadOnly, graphPosition } = useGraph() const { isReadOnly, graphPosition } = useGraph()
const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState< const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState<
number | undefined number | undefined
@ -45,17 +45,18 @@ export const StepNodesList = ({
x: 0, x: 0,
y: 0, y: 0,
}) })
const isDraggingOnCurrentBlock = const isDraggingOnCurrentGroup =
(draggedStep || draggedStepType) && mouseOverBlock?.id === blockId (draggedBlock || draggedBlockType) && mouseOverGroup?.id === groupId
const showSortPlaceholders = !isStartBlock && (draggedStep || draggedStepType) const showSortPlaceholders =
!isStartGroup && (draggedBlock || draggedBlockType)
useEffect(() => { useEffect(() => {
if (mouseOverBlock?.id !== blockId) setExpandedPlaceholderIndex(undefined) if (mouseOverGroup?.id !== groupId) setExpandedPlaceholderIndex(undefined)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [mouseOverBlock?.id]) }, [mouseOverGroup?.id])
const handleMouseMoveGlobal = (event: MouseEvent) => { const handleMouseMoveGlobal = (event: MouseEvent) => {
if (!draggedStep || draggedStep.blockId !== blockId) return if (!draggedBlock || draggedBlock.groupId !== groupId) return
const { clientX, clientY } = event const { clientX, clientY } = event
setPosition({ setPosition({
x: clientX - mousePositionInElement.x, x: clientX - mousePositionInElement.x,
@ -63,41 +64,44 @@ export const StepNodesList = ({
}) })
} }
const handleMouseMoveOnBlock = (event: MouseEvent) => { const handleMouseMoveOnGroup = (event: MouseEvent) => {
if (!isDraggingOnCurrentBlock) return if (!isDraggingOnCurrentGroup) return
setExpandedPlaceholderIndex( setExpandedPlaceholderIndex(
computeNearestPlaceholderIndex(event.pageY, placeholderRefs) computeNearestPlaceholderIndex(event.pageY, placeholderRefs)
) )
} }
const handleMouseUpOnBlock = (e: MouseEvent) => { const handleMouseUpOnGroup = (e: MouseEvent) => {
setExpandedPlaceholderIndex(undefined) setExpandedPlaceholderIndex(undefined)
if (!isDraggingOnCurrentBlock) return if (!isDraggingOnCurrentGroup) return
const stepIndex = computeNearestPlaceholderIndex(e.clientY, placeholderRefs) const blockIndex = computeNearestPlaceholderIndex(
createStep( e.clientY,
blockId, placeholderRefs
(draggedStep || draggedStepType) as DraggableStep | DraggableStepType, )
createBlock(
groupId,
(draggedBlock || draggedBlockType) as DraggableBlock | DraggableBlockType,
{ {
groupIndex,
blockIndex, blockIndex,
stepIndex,
} }
) )
setDraggedStep(undefined) setDraggedBlock(undefined)
setDraggedStepType(undefined) setDraggedBlockType(undefined)
} }
const handleStepMouseDown = const handleBlockMouseDown =
(stepIndex: number) => (blockIndex: number) =>
( (
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates }, { absolute, relative }: { absolute: Coordinates; relative: Coordinates },
step: DraggableStep block: DraggableBlock
) => { ) => {
if (isReadOnly) return if (isReadOnly) return
placeholderRefs.current.splice(stepIndex + 1, 1) placeholderRefs.current.splice(blockIndex + 1, 1)
detachStepFromBlock({ blockIndex, stepIndex }) detachBlockFromGroup({ groupIndex, blockIndex })
setPosition(absolute) setPosition(absolute)
setMousePositionInElement(relative) setMousePositionInElement(relative)
setDraggedStep(step) setDraggedBlock(block)
} }
const handlePushElementRef = const handlePushElementRef =
@ -106,11 +110,11 @@ export const StepNodesList = ({
} }
useEventListener('mousemove', handleMouseMoveGlobal) useEventListener('mousemove', handleMouseMoveGlobal)
useEventListener('mousemove', handleMouseMoveOnBlock, blockRef.current) useEventListener('mousemove', handleMouseMoveOnGroup, groupRef.current)
useEventListener( useEventListener(
'mouseup', 'mouseup',
handleMouseUpOnBlock, handleMouseUpOnGroup,
mouseOverBlock?.ref.current, mouseOverGroup?.ref.current,
{ {
capture: true, capture: true,
} }
@ -119,7 +123,7 @@ export const StepNodesList = ({
<Stack <Stack
spacing={1} spacing={1}
transition="none" transition="none"
pointerEvents={isReadOnly || isStartBlock ? 'none' : 'auto'} pointerEvents={isReadOnly || isStartGroup ? 'none' : 'auto'}
> >
<Flex <Flex
ref={handlePushElementRef(0)} ref={handlePushElementRef(0)}
@ -134,14 +138,14 @@ export const StepNodesList = ({
transition={showSortPlaceholders ? 'height 200ms' : 'none'} transition={showSortPlaceholders ? 'height 200ms' : 'none'}
/> />
{typebot && {typebot &&
steps.map((step, idx) => ( blocks.map((block, idx) => (
<Stack key={step.id} spacing={1}> <Stack key={block.id} spacing={1}>
<StepNode <BlockNode
key={step.id} key={block.id}
step={step} block={block}
indices={{ blockIndex, stepIndex: idx }} indices={{ groupIndex, blockIndex: idx }}
isConnectable={steps.length - 1 === idx} isConnectable={blocks.length - 1 === idx}
onMouseDown={handleStepMouseDown(idx)} onMouseDown={handleBlockMouseDown(idx)}
/> />
<Flex <Flex
ref={handlePushElementRef(idx + 1)} ref={handlePushElementRef(idx + 1)}
@ -157,11 +161,11 @@ export const StepNodesList = ({
/> />
</Stack> </Stack>
))} ))}
{draggedStep && draggedStep.blockId === blockId && ( {draggedBlock && draggedBlock.groupId === groupId && (
<Portal> <Portal>
<StepNodeOverlay <BlockNodeOverlay
step={draggedStep} block={draggedBlock}
indices={{ blockIndex, stepIndex: 0 }} indices={{ groupIndex, blockIndex: 0 }}
pos="fixed" pos="fixed"
top="0" top="0"
left="0" left="0"

View File

@ -6,18 +6,18 @@ import {
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { ImageUploadContent } from 'components/shared/ImageUploadContent' import { ImageUploadContent } from 'components/shared/ImageUploadContent'
import { import {
BubbleStep, BubbleBlock,
BubbleStepContent, BubbleBlockContent,
BubbleStepType, BubbleBlockType,
TextBubbleStep, TextBubbleBlock,
} from 'models' } from 'models'
import { useRef } from 'react' import { useRef } from 'react'
import { EmbedUploadContent } from './EmbedUploadContent' import { EmbedUploadContent } from './EmbedUploadContent'
import { VideoUploadContent } from './VideoUploadContent' import { VideoUploadContent } from './VideoUploadContent'
type Props = { type Props = {
step: Exclude<BubbleStep, TextBubbleStep> block: Exclude<BubbleBlock, TextBubbleBlock>
onContentChange: (content: BubbleStepContent) => void onContentChange: (content: BubbleBlockContent) => void
} }
export const MediaBubblePopoverContent = (props: Props) => { export const MediaBubblePopoverContent = (props: Props) => {
@ -28,7 +28,7 @@ export const MediaBubblePopoverContent = (props: Props) => {
<Portal> <Portal>
<PopoverContent <PopoverContent
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
w={props.step.type === BubbleStepType.IMAGE ? '500px' : '400px'} w={props.block.type === BubbleBlockType.IMAGE ? '500px' : '400px'}
> >
<PopoverArrow /> <PopoverArrow />
<PopoverBody ref={ref} shadow="lg"> <PopoverBody ref={ref} shadow="lg">
@ -39,26 +39,32 @@ export const MediaBubblePopoverContent = (props: Props) => {
) )
} }
export const MediaBubbleContent = ({ step, onContentChange }: Props) => { export const MediaBubbleContent = ({ block, onContentChange }: Props) => {
const handleImageUrlChange = (url: string) => onContentChange({ url }) const handleImageUrlChange = (url: string) => onContentChange({ url })
switch (step.type) { switch (block.type) {
case BubbleStepType.IMAGE: { case BubbleBlockType.IMAGE: {
return ( return (
<ImageUploadContent <ImageUploadContent
url={step.content?.url} url={block.content?.url}
onSubmit={handleImageUrlChange} onSubmit={handleImageUrlChange}
/> />
) )
} }
case BubbleStepType.VIDEO: { case BubbleBlockType.VIDEO: {
return ( return (
<VideoUploadContent content={step.content} onSubmit={onContentChange} /> <VideoUploadContent
content={block.content}
onSubmit={onContentChange}
/>
) )
} }
case BubbleStepType.EMBED: { case BubbleBlockType.EMBED: {
return ( return (
<EmbedUploadContent content={step.content} onSubmit={onContentChange} /> <EmbedUploadContent
content={block.content}
onSubmit={onContentChange}
/>
) )
} }
} }

View File

@ -9,13 +9,13 @@ import {
import { ExpandIcon } from 'assets/icons' import { ExpandIcon } from 'assets/icons'
import { import {
ConditionItem, ConditionItem,
ConditionStep, ConditionBlock,
InputStepType, InputBlockType,
IntegrationStepType, IntegrationBlockType,
LogicStepType, LogicBlockType,
Step, Block,
StepOptions, BlockOptions,
StepWithOptions, BlockWithOptions,
Webhook, Webhook,
} from 'models' } from 'models'
import { useRef } from 'react' import { useRef } from 'react'
@ -42,10 +42,10 @@ import { WebhookSettings } from './bodies/WebhookSettings'
import { ZapierSettings } from './bodies/ZapierSettings' import { ZapierSettings } from './bodies/ZapierSettings'
type Props = { type Props = {
step: StepWithOptions | ConditionStep block: BlockWithOptions | ConditionBlock
webhook?: Webhook webhook?: Webhook
onExpandClick: () => void onExpandClick: () => void
onStepChange: (updates: Partial<Step>) => void onBlockChange: (updates: Partial<Block>) => void
} }
export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => { export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
@ -68,7 +68,7 @@ export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
ref={ref} ref={ref}
shadow="lg" shadow="lg"
> >
<StepSettings {...props} /> <BlockSettings {...props} />
</PopoverBody> </PopoverBody>
<IconButton <IconButton
pos="absolute" pos="absolute"
@ -84,156 +84,156 @@ export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
) )
} }
export const StepSettings = ({ export const BlockSettings = ({
step, block,
onStepChange, onBlockChange,
}: { }: {
step: StepWithOptions | ConditionStep block: BlockWithOptions | ConditionBlock
webhook?: Webhook webhook?: Webhook
onStepChange: (step: Partial<Step>) => void onBlockChange: (block: Partial<Block>) => void
}): JSX.Element => { }): JSX.Element => {
const handleOptionsChange = (options: StepOptions) => { const handleOptionsChange = (options: BlockOptions) => {
onStepChange({ options } as Partial<Step>) onBlockChange({ options } as Partial<Block>)
} }
const handleItemChange = (updates: Partial<ConditionItem>) => { const handleItemChange = (updates: Partial<ConditionItem>) => {
onStepChange({ onBlockChange({
items: [{ ...(step as ConditionStep).items[0], ...updates }], items: [{ ...(block as ConditionBlock).items[0], ...updates }],
} as Partial<Step>) } as Partial<Block>)
} }
switch (step.type) { switch (block.type) {
case InputStepType.TEXT: { case InputBlockType.TEXT: {
return ( return (
<TextInputSettingsBody <TextInputSettingsBody
options={step.options} options={block.options}
onOptionsChange={handleOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
case InputStepType.NUMBER: { case InputBlockType.NUMBER: {
return ( return (
<NumberInputSettingsBody <NumberInputSettingsBody
options={step.options} options={block.options}
onOptionsChange={handleOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
case InputStepType.EMAIL: { case InputBlockType.EMAIL: {
return ( return (
<EmailInputSettingsBody <EmailInputSettingsBody
options={step.options} options={block.options}
onOptionsChange={handleOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
case InputStepType.URL: { case InputBlockType.URL: {
return ( return (
<UrlInputSettingsBody <UrlInputSettingsBody
options={step.options} options={block.options}
onOptionsChange={handleOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
case InputStepType.DATE: { case InputBlockType.DATE: {
return ( return (
<DateInputSettingsBody <DateInputSettingsBody
options={step.options} options={block.options}
onOptionsChange={handleOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
case InputStepType.PHONE: { case InputBlockType.PHONE: {
return ( return (
<PhoneNumberSettingsBody <PhoneNumberSettingsBody
options={step.options} options={block.options}
onOptionsChange={handleOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
case InputStepType.CHOICE: { case InputBlockType.CHOICE: {
return ( return (
<ChoiceInputSettingsBody <ChoiceInputSettingsBody
options={step.options} options={block.options}
onOptionsChange={handleOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
case InputStepType.PAYMENT: { case InputBlockType.PAYMENT: {
return ( return (
<PaymentSettings <PaymentSettings
options={step.options} options={block.options}
onOptionsChange={handleOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
case InputStepType.RATING: { case InputBlockType.RATING: {
return ( return (
<RatingInputSettings <RatingInputSettings
options={step.options} options={block.options}
onOptionsChange={handleOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
case LogicStepType.SET_VARIABLE: { case LogicBlockType.SET_VARIABLE: {
return ( return (
<SetVariableSettings <SetVariableSettings
options={step.options} options={block.options}
onOptionsChange={handleOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
case LogicStepType.CONDITION: { case LogicBlockType.CONDITION: {
return ( return (
<ConditionSettingsBody step={step} onItemChange={handleItemChange} /> <ConditionSettingsBody block={block} onItemChange={handleItemChange} />
) )
} }
case LogicStepType.REDIRECT: { case LogicBlockType.REDIRECT: {
return ( return (
<RedirectSettings <RedirectSettings
options={step.options} options={block.options}
onOptionsChange={handleOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
case LogicStepType.CODE: { case LogicBlockType.CODE: {
return ( return (
<CodeSettings <CodeSettings
options={step.options} options={block.options}
onOptionsChange={handleOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
case LogicStepType.TYPEBOT_LINK: { case LogicBlockType.TYPEBOT_LINK: {
return ( return (
<TypebotLinkSettingsForm <TypebotLinkSettingsForm
options={step.options} options={block.options}
onOptionsChange={handleOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
case IntegrationStepType.GOOGLE_SHEETS: { case IntegrationBlockType.GOOGLE_SHEETS: {
return ( return (
<GoogleSheetsSettingsBody <GoogleSheetsSettingsBody
options={step.options} options={block.options}
onOptionsChange={handleOptionsChange} onOptionsChange={handleOptionsChange}
stepId={step.id} blockId={block.id}
/> />
) )
} }
case IntegrationStepType.GOOGLE_ANALYTICS: { case IntegrationBlockType.GOOGLE_ANALYTICS: {
return ( return (
<GoogleAnalyticsSettings <GoogleAnalyticsSettings
options={step.options} options={block.options}
onOptionsChange={handleOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )
} }
case IntegrationStepType.ZAPIER: { case IntegrationBlockType.ZAPIER: {
return <ZapierSettings step={step} /> return <ZapierSettings block={block} />
} }
case IntegrationStepType.MAKE_COM: { case IntegrationBlockType.MAKE_COM: {
return ( return (
<WebhookSettings <WebhookSettings
step={step} block={block}
onOptionsChange={handleOptionsChange} onOptionsChange={handleOptionsChange}
provider={{ provider={{
name: 'Make.com', name: 'Make.com',
@ -242,10 +242,10 @@ export const StepSettings = ({
/> />
) )
} }
case IntegrationStepType.PABBLY_CONNECT: { case IntegrationBlockType.PABBLY_CONNECT: {
return ( return (
<WebhookSettings <WebhookSettings
step={step} block={block}
onOptionsChange={handleOptionsChange} onOptionsChange={handleOptionsChange}
provider={{ provider={{
name: 'Pabbly Connect', name: 'Pabbly Connect',
@ -254,15 +254,15 @@ export const StepSettings = ({
/> />
) )
} }
case IntegrationStepType.WEBHOOK: { case IntegrationBlockType.WEBHOOK: {
return ( return (
<WebhookSettings step={step} onOptionsChange={handleOptionsChange} /> <WebhookSettings block={block} onOptionsChange={handleOptionsChange} />
) )
} }
case IntegrationStepType.EMAIL: { case IntegrationBlockType.EMAIL: {
return ( return (
<SendEmailSettings <SendEmailSettings
options={step.options} options={block.options}
onOptionsChange={handleOptionsChange} onOptionsChange={handleOptionsChange}
/> />
) )

View File

@ -4,22 +4,22 @@ import { TableList } from 'components/shared/TableList'
import { import {
Comparison, Comparison,
ConditionItem, ConditionItem,
ConditionStep, ConditionBlock,
LogicalOperator, LogicalOperator,
} from 'models' } from 'models'
import React from 'react' import React from 'react'
import { ComparisonItem } from './ComparisonsItem' import { ComparisonItem } from './ComparisonsItem'
type ConditionSettingsBodyProps = { type ConditionSettingsBodyProps = {
step: ConditionStep block: ConditionBlock
onItemChange: (updates: Partial<ConditionItem>) => void onItemChange: (updates: Partial<ConditionItem>) => void
} }
export const ConditionSettingsBody = ({ export const ConditionSettingsBody = ({
step, block,
onItemChange, onItemChange,
}: ConditionSettingsBodyProps) => { }: ConditionSettingsBodyProps) => {
const itemContent = step.items[0].content const itemContent = block.items[0].content
const handleComparisonsChange = (comparisons: Comparison[]) => const handleComparisonsChange = (comparisons: Comparison[]) =>
onItemChange({ content: { ...itemContent, comparisons } }) onItemChange({ content: { ...itemContent, comparisons } })

View File

@ -21,11 +21,15 @@ import { getGoogleSheetsConsentScreenUrl } from 'services/integrations'
type Props = { type Props = {
isOpen: boolean isOpen: boolean
stepId: string blockId: string
onClose: () => void onClose: () => void
} }
export const GoogleSheetConnectModal = ({ stepId, isOpen, onClose }: Props) => { export const GoogleSheetConnectModal = ({
blockId,
isOpen,
onClose,
}: Props) => {
const { workspace } = useWorkspace() const { workspace } = useWorkspace()
return ( return (
<Modal isOpen={isOpen} onClose={onClose} size="lg"> <Modal isOpen={isOpen} onClose={onClose} size="lg">
@ -56,7 +60,7 @@ export const GoogleSheetConnectModal = ({ stepId, isOpen, onClose }: Props) => {
variant="outline" variant="outline"
href={getGoogleSheetsConsentScreenUrl( href={getGoogleSheetsConsentScreenUrl(
window.location.href, window.location.href,
stepId, blockId,
workspace?.id workspace?.id
)} )}
mx="auto" mx="auto"

View File

@ -25,13 +25,13 @@ import { GoogleSheetConnectModal } from './GoogleSheetsConnectModal'
type Props = { type Props = {
options: GoogleSheetsOptions options: GoogleSheetsOptions
onOptionsChange: (options: GoogleSheetsOptions) => void onOptionsChange: (options: GoogleSheetsOptions) => void
stepId: string blockId: string
} }
export const GoogleSheetsSettingsBody = ({ export const GoogleSheetsSettingsBody = ({
options, options,
onOptionsChange, onOptionsChange,
stepId, blockId,
}: Props) => { }: Props) => {
const { save } = useTypebot() const { save } = useTypebot()
const { sheets, isLoading } = useSheets({ const { sheets, isLoading } = useSheets({
@ -93,7 +93,7 @@ export const GoogleSheetsSettingsBody = ({
onCreateNewClick={handleCreateNewClick} onCreateNewClick={handleCreateNewClick}
/> />
<GoogleSheetConnectModal <GoogleSheetConnectModal
stepId={stepId} blockId={blockId}
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
/> />

View File

@ -23,8 +23,8 @@ export const NumberInputSettingsBody = ({
onOptionsChange(removeUndefinedFields({ ...options, min })) onOptionsChange(removeUndefinedFields({ ...options, min }))
const handleMaxChange = (max?: number) => const handleMaxChange = (max?: number) =>
onOptionsChange(removeUndefinedFields({ ...options, max })) onOptionsChange(removeUndefinedFields({ ...options, max }))
const handleStepChange = (step?: number) => const handleBlockChange = (block?: number) =>
onOptionsChange(removeUndefinedFields({ ...options, step })) onOptionsChange(removeUndefinedFields({ ...options, block }))
const handleVariableChange = (variable?: Variable) => { const handleVariableChange = (variable?: Variable) => {
onOptionsChange({ ...options, variableId: variable?.id }) onOptionsChange({ ...options, variableId: variable?.id })
} }
@ -78,7 +78,7 @@ export const NumberInputSettingsBody = ({
<SmartNumberInput <SmartNumberInput
id="step" id="step"
value={options.step} value={options.step}
onValueChange={handleStepChange} onValueChange={handleBlockChange}
/> />
</HStack> </HStack>
<Stack> <Stack>

View File

@ -0,0 +1,41 @@
import { Input } from '@chakra-ui/react'
import { SearchableDropdown } from 'components/shared/SearchableDropdown'
import { Group } from 'models'
import { useMemo } from 'react'
import { byId } from 'utils'
type Props = {
groups: Group[]
groupId?: string
onGroupIdSelected: (groupId: string) => void
isLoading?: boolean
}
export const GroupsDropdown = ({
groups,
groupId,
onGroupIdSelected,
isLoading,
}: Props) => {
const currentGroup = useMemo(
() => groups?.find(byId(groupId)),
[groupId, groups]
)
const handleGroupSelect = (title: string) => {
const id = groups?.find((b) => b.title === title)?.id
if (id) onGroupIdSelected(id)
}
if (isLoading) return <Input value="Loading..." isDisabled />
if (!groups || groups.length === 0)
return <Input value="No groups found" isDisabled />
return (
<SearchableDropdown
selectedItem={currentGroup?.title}
items={(groups ?? []).map((b) => b.title)}
onValueChange={handleGroupSelect}
placeholder={'Select a block'}
/>
)
}

View File

@ -2,7 +2,7 @@ import { Stack } from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { TypebotLinkOptions } from 'models' import { TypebotLinkOptions } from 'models'
import { byId } from 'utils' import { byId } from 'utils'
import { BlocksDropdown } from './BlocksDropdown' import { GroupsDropdown } from './GroupsDropdown'
import { TypebotsDropdown } from './TypebotsDropdown' import { TypebotsDropdown } from './TypebotsDropdown'
type Props = { type Props = {
@ -18,8 +18,8 @@ export const TypebotLinkSettingsForm = ({
const handleTypebotIdChange = (typebotId: string | 'current') => const handleTypebotIdChange = (typebotId: string | 'current') =>
onOptionsChange({ ...options, typebotId }) onOptionsChange({ ...options, typebotId })
const handleBlockIdChange = (blockId: string) => const handleGroupIdChange = (groupId: string) =>
onOptionsChange({ ...options, blockId }) onOptionsChange({ ...options, groupId })
return ( return (
<Stack> <Stack>
@ -30,15 +30,15 @@ export const TypebotLinkSettingsForm = ({
currentWorkspaceId={typebot.workspaceId as string} currentWorkspaceId={typebot.workspaceId as string}
/> />
)} )}
<BlocksDropdown <GroupsDropdown
blocks={ groups={
typebot && typebot &&
(options.typebotId === typebot.id || options.typebotId === 'current') (options.typebotId === typebot.id || options.typebotId === 'current')
? typebot.blocks ? typebot.groups
: linkedTypebots?.find(byId(options.typebotId))?.blocks ?? [] : linkedTypebots?.find(byId(options.typebotId))?.groups ?? []
} }
blockId={options.blockId} groupId={options.groupId}
onBlockIdSelected={handleBlockIdChange} onGroupIdSelected={handleGroupIdChange}
isLoading={ isLoading={
linkedTypebots === undefined && linkedTypebots === undefined &&
typebot && typebot &&

View File

@ -22,11 +22,11 @@ import {
WebhookOptions, WebhookOptions,
VariableForTest, VariableForTest,
ResponseVariableMapping, ResponseVariableMapping,
WebhookStep, WebhookBlock,
defaultWebhookAttributes, defaultWebhookAttributes,
Webhook, Webhook,
MakeComStep, MakeComBlock,
PabblyConnectStep, PabblyConnectBlock,
} from 'models' } from 'models'
import { DropdownList } from 'components/shared/DropdownList' import { DropdownList } from 'components/shared/DropdownList'
import { TableList, TableListItemProps } from 'components/shared/TableList' import { TableList, TableListItemProps } from 'components/shared/TableList'
@ -49,13 +49,13 @@ type Provider = {
url: string url: string
} }
type Props = { type Props = {
step: WebhookStep | MakeComStep | PabblyConnectStep block: WebhookBlock | MakeComBlock | PabblyConnectBlock
onOptionsChange: (options: WebhookOptions) => void onOptionsChange: (options: WebhookOptions) => void
provider?: Provider provider?: Provider
} }
export const WebhookSettings = ({ export const WebhookSettings = ({
step: { options, blockId, id: stepId, webhookId }, block: { options, id: blockId, webhookId },
onOptionsChange, onOptionsChange,
provider, provider,
}: Props) => { }: Props) => {
@ -135,7 +135,7 @@ export const WebhookSettings = ({
options.variablesForTest, options.variablesForTest,
typebot.variables typebot.variables
), ),
{ blockId, stepId } { blockId }
) )
if (error) if (error)
return showToast({ title: error.name, description: error.message }) return showToast({ title: error.name, description: error.message })

View File

@ -9,17 +9,17 @@ import {
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { ExternalLinkIcon } from 'assets/icons' import { ExternalLinkIcon } from 'assets/icons'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { ZapierStep } from 'models' import { ZapierBlock } from 'models'
import React from 'react' import React from 'react'
import { byId } from 'utils' import { byId } from 'utils'
type Props = { type Props = {
step: ZapierStep block: ZapierBlock
} }
export const ZapierSettings = ({ step }: Props) => { export const ZapierSettings = ({ block }: Props) => {
const { webhooks } = useTypebot() const { webhooks } = useTypebot()
const webhook = webhooks.find(byId(step.webhookId)) const webhook = webhooks.find(byId(block.webhookId))
return ( return (
<Stack spacing={4}> <Stack spacing={4}>
@ -33,7 +33,7 @@ export const ZapierSettings = ({ step }: Props) => {
<>Your zap is correctly configured 🚀</> <>Your zap is correctly configured 🚀</>
) : ( ) : (
<Stack> <Stack>
<Text>Head up to Zapier to configure this step:</Text> <Text>Head up to Zapier to configure this block:</Text>
<Button <Button
as={Link} as={Link}
href="https://zapier.com/apps/typebot/integrations" href="https://zapier.com/apps/typebot/integrations"

View File

@ -40,7 +40,7 @@ export const TextBubbleEditor = ({ initialValue, onClose }: Props) => {
const textEditorRef = useRef<HTMLDivElement>(null) const textEditorRef = useRef<HTMLDivElement>(null)
const closeEditor = () => onClose(convertValueToStepContent(value)) const closeEditor = () => onClose(convertValueToBlockContent(value))
useOutsideClick({ useOutsideClick({
ref: textEditorRef, ref: textEditorRef,
@ -70,7 +70,7 @@ export const TextBubbleEditor = ({ initialValue, onClose }: Props) => {
} }
} }
const convertValueToStepContent = (value: TElement[]): TextBubbleContent => { const convertValueToBlockContent = (value: TElement[]): TextBubbleContent => {
if (value.length === 0) defaultTextBubbleContent if (value.length === 0) defaultTextBubbleContent
const html = serializeHtml(editor, { const html = serializeHtml(editor, {
nodes: value, nodes: value,

View File

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

View File

@ -0,0 +1,193 @@
import {
Editable,
EditableInput,
EditablePreview,
IconButton,
Stack,
} from '@chakra-ui/react'
import React, { useEffect, useRef, useState } from 'react'
import { Group } from 'models'
import { useGraph } from 'contexts/GraphContext'
import { useBlockDnd } from 'contexts/GraphDndContext'
import { BlockNodesList } from '../BlockNode/BlockNodesList'
import { isDefined, isNotDefined } from 'utils'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { ContextMenu } from 'components/shared/ContextMenu'
import { GroupNodeContextMenu } from './GroupNodeContextMenu'
import { useDebounce } from 'use-debounce'
import { setMultipleRefs } from 'services/utils'
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'
import { PlayIcon } from 'assets/icons'
import { RightPanel, useEditor } from 'contexts/EditorContext'
type Props = {
group: Group
groupIndex: number
}
export const GroupNode = ({ group, groupIndex }: Props) => {
const {
connectingIds,
setConnectingIds,
previewingEdge,
groupsCoordinates,
updateGroupCoordinates,
isReadOnly,
focusedGroupId,
setFocusedGroupId,
graphPosition,
} = useGraph()
const { typebot, updateGroup } = useTypebot()
const { setMouseOverGroup, mouseOverGroup } = useBlockDnd()
const [isMouseDown, setIsMouseDown] = useState(false)
const [isConnecting, setIsConnecting] = useState(false)
const { setRightPanel, setStartPreviewAtGroup } = useEditor()
const isPreviewing =
previewingEdge?.from.groupId === group.id ||
(previewingEdge?.to.groupId === group.id &&
isNotDefined(previewingEdge.to.groupId))
const isStartGroup =
isDefined(group.blocks[0]) && group.blocks[0].type === 'start'
const groupCoordinates = groupsCoordinates[group.id]
const groupRef = useRef<HTMLDivElement | null>(null)
const [debouncedGroupPosition] = useDebounce(groupCoordinates, 100)
useEffect(() => {
if (!debouncedGroupPosition || isReadOnly) return
if (
debouncedGroupPosition?.x === group.graphCoordinates.x &&
debouncedGroupPosition.y === group.graphCoordinates.y
)
return
updateGroup(groupIndex, { graphCoordinates: debouncedGroupPosition })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedGroupPosition])
useEffect(() => {
setIsConnecting(
connectingIds?.target?.groupId === group.id &&
isNotDefined(connectingIds.target?.groupId)
)
}, [connectingIds, group.id])
const handleTitleSubmit = (title: string) =>
updateGroup(groupIndex, { title })
const handleMouseDown = (e: React.MouseEvent) => {
e.stopPropagation()
}
const handleMouseEnter = () => {
if (isReadOnly) return
if (mouseOverGroup?.id !== group.id && !isStartGroup)
setMouseOverGroup({ id: group.id, ref: groupRef })
if (connectingIds)
setConnectingIds({ ...connectingIds, target: { groupId: group.id } })
}
const handleMouseLeave = () => {
if (isReadOnly) return
setMouseOverGroup(undefined)
if (connectingIds) setConnectingIds({ ...connectingIds, target: undefined })
}
const onDrag = (_: DraggableEvent, draggableData: DraggableData) => {
const { deltaX, deltaY } = draggableData
updateGroupCoordinates(group.id, {
x: groupCoordinates.x + deltaX / graphPosition.scale,
y: groupCoordinates.y + deltaY / graphPosition.scale,
})
}
const onDragStart = () => {
setFocusedGroupId(group.id)
setIsMouseDown(true)
}
const startPreviewAtThisGroup = () => {
setStartPreviewAtGroup(group.id)
setRightPanel(RightPanel.PREVIEW)
}
const onDragStop = () => setIsMouseDown(false)
return (
<ContextMenu<HTMLDivElement>
renderMenu={() => <GroupNodeContextMenu groupIndex={groupIndex} />}
isDisabled={isReadOnly || isStartGroup}
>
{(ref, isOpened) => (
<DraggableCore
enableUserSelectHack={false}
onDrag={onDrag}
onStart={onDragStart}
onStop={onDragStop}
onMouseDown={(e) => e.stopPropagation()}
>
<Stack
ref={setMultipleRefs([ref, groupRef])}
data-testid="group"
p="4"
rounded="xl"
bgColor="#ffffff"
borderWidth="2px"
borderColor={
isConnecting || isOpened || isPreviewing ? 'blue.400' : '#ffffff'
}
w="300px"
transition="border 300ms, box-shadow 200ms"
pos="absolute"
style={{
transform: `translate(${groupCoordinates?.x ?? 0}px, ${
groupCoordinates?.y ?? 0
}px)`,
}}
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
cursor={isMouseDown ? 'grabbing' : 'pointer'}
shadow="md"
_hover={{ shadow: 'lg' }}
zIndex={focusedGroupId === group.id ? 10 : 1}
>
<Editable
defaultValue={group.title}
onSubmit={handleTitleSubmit}
fontWeight="semibold"
pointerEvents={isReadOnly || isStartGroup ? 'none' : 'auto'}
>
<EditablePreview
_hover={{ bgColor: 'gray.200' }}
px="1"
userSelect={'none'}
/>
<EditableInput
minW="0"
px="1"
onMouseDown={(e) => e.stopPropagation()}
/>
</Editable>
{typebot && (
<BlockNodesList
groupId={group.id}
blocks={group.blocks}
groupIndex={groupIndex}
groupRef={ref}
isStartGroup={isStartGroup}
/>
)}
<IconButton
icon={<PlayIcon />}
aria-label={'Preview bot from this group'}
pos="absolute"
right={2}
top={0}
size="sm"
variant="outline"
onClick={startPreviewAtThisGroup}
/>
</Stack>
</DraggableCore>
)}
</ContextMenu>
)
}

View File

@ -1,15 +1,17 @@
import { MenuList, MenuItem } from '@chakra-ui/react' import { MenuList, MenuItem } from '@chakra-ui/react'
import { CopyIcon, TrashIcon } from 'assets/icons' import { CopyIcon, TrashIcon } from 'assets/icons'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext' import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { StepIndices } from 'models'
type Props = { indices: StepIndices } export const GroupNodeContextMenu = ({
export const StepNodeContextMenu = ({ indices }: Props) => { groupIndex,
const { deleteStep, duplicateStep } = useTypebot() }: {
groupIndex: number
}) => {
const { deleteGroup, duplicateGroup } = useTypebot()
const handleDuplicateClick = () => duplicateStep(indices) const handleDeleteClick = () => deleteGroup(groupIndex)
const handleDeleteClick = () => deleteStep(indices) const handleDuplicateClick = () => duplicateGroup(groupIndex)
return ( return (
<MenuList> <MenuList>

View File

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

View File

@ -5,7 +5,7 @@ import { NodePosition, useDragDistance } from 'contexts/GraphDndContext'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { import {
ButtonItem, ButtonItem,
ChoiceInputStep, ChoiceInputBlock,
Item, Item,
ItemIndices, ItemIndices,
ItemType, ItemType,
@ -21,7 +21,7 @@ type Props = {
indices: ItemIndices indices: ItemIndices
isReadOnly: boolean isReadOnly: boolean
onMouseDown?: ( onMouseDown?: (
stepNodePosition: { absolute: Coordinates; relative: Coordinates }, blockNodePosition: { absolute: Coordinates; relative: Coordinates },
item: ButtonItem item: ButtonItem
) => void ) => void
} }
@ -33,9 +33,9 @@ export const ItemNode = ({ item, indices, isReadOnly, onMouseDown }: Props) => {
const itemRef = useRef<HTMLDivElement | null>(null) const itemRef = useRef<HTMLDivElement | null>(null)
const isPreviewing = previewingEdge?.from.itemId === item.id const isPreviewing = previewingEdge?.from.itemId === item.id
const isConnectable = !( const isConnectable = !(
typebot?.blocks[indices.blockIndex].steps[ typebot?.groups[indices.groupIndex].blocks[
indices.stepIndex indices.blockIndex
] as ChoiceInputStep ] as ChoiceInputBlock
)?.options?.isMultipleChoice )?.options?.isMultipleChoice
const onDrag = (position: NodePosition) => { const onDrag = (position: NodePosition) => {
if (!onMouseDown || item.type !== ItemType.BUTTON) return if (!onMouseDown || item.type !== ItemType.BUTTON) return
@ -83,8 +83,8 @@ export const ItemNode = ({ item, indices, isReadOnly, onMouseDown }: Props) => {
{typebot && isConnectable && ( {typebot && isConnectable && (
<SourceEndpoint <SourceEndpoint
source={{ source={{
blockId: typebot.blocks[indices.blockIndex].id, groupId: typebot.groups[indices.groupIndex].id,
stepId: item.stepId, blockId: item.blockId,
itemId: item.id, itemId: item.id,
}} }}
pos="absolute" pos="absolute"

View File

@ -45,7 +45,7 @@ export const ButtonNodeContent = ({ item, indices, isMouseOver }: Props) => {
const handlePlusClick = () => { const handlePlusClick = () => {
const itemIndex = indices.itemIndex + 1 const itemIndex = indices.itemIndex + 1
createItem( createItem(
{ stepId: item.stepId, type: ItemType.BUTTON }, { blockId: item.blockId, type: ItemType.BUTTON },
{ ...indices, itemIndex } { ...indices, itemIndex }
) )
} }

View File

@ -1,38 +1,38 @@
import { Flex, Portal, Stack, Text, useEventListener } from '@chakra-ui/react' import { Flex, Portal, Stack, Text, useEventListener } from '@chakra-ui/react'
import { import {
computeNearestPlaceholderIndex, computeNearestPlaceholderIndex,
useStepDnd, useBlockDnd,
} from 'contexts/GraphDndContext' } from 'contexts/GraphDndContext'
import { Coordinates, useGraph } from 'contexts/GraphContext' import { Coordinates, useGraph } from 'contexts/GraphContext'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
import { ButtonItem, StepIndices, StepWithItems } from 'models' import { ButtonItem, BlockIndices, BlockWithItems } from 'models'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { ItemNode } from './ItemNode' import { ItemNode } from './ItemNode'
import { SourceEndpoint } from '../../Endpoints' import { SourceEndpoint } from '../../Endpoints'
import { ItemNodeOverlay } from './ItemNodeOverlay' import { ItemNodeOverlay } from './ItemNodeOverlay'
type Props = { type Props = {
step: StepWithItems block: BlockWithItems
indices: StepIndices indices: BlockIndices
isReadOnly?: boolean isReadOnly?: boolean
} }
export const ItemNodesList = ({ export const ItemNodesList = ({
step, block,
indices: { blockIndex, stepIndex }, indices: { groupIndex, blockIndex },
isReadOnly = false, isReadOnly = false,
}: Props) => { }: Props) => {
const { typebot, createItem, detachItemFromStep } = useTypebot() const { typebot, createItem, detachItemFromBlock } = useTypebot()
const { draggedItem, setDraggedItem, mouseOverBlock } = useStepDnd() const { draggedItem, setDraggedItem, mouseOverGroup } = useBlockDnd()
const placeholderRefs = useRef<HTMLDivElement[]>([]) const placeholderRefs = useRef<HTMLDivElement[]>([])
const { graphPosition } = useGraph() const { graphPosition } = useGraph()
const blockId = typebot?.blocks[blockIndex].id const groupId = typebot?.groups[groupIndex].id
const isDraggingOnCurrentBlock = const isDraggingOnCurrentGroup =
(draggedItem && mouseOverBlock?.id === blockId) ?? false (draggedItem && mouseOverGroup?.id === groupId) ?? false
const showPlaceholders = draggedItem && !isReadOnly const showPlaceholders = draggedItem && !isReadOnly
const isLastStep = const isLastBlock =
typebot?.blocks[blockIndex].steps[stepIndex + 1] === undefined typebot?.groups[groupIndex].blocks[blockIndex + 1] === undefined
const [position, setPosition] = useState({ const [position, setPosition] = useState({
x: 0, x: 0,
@ -44,7 +44,7 @@ export const ItemNodesList = ({
>() >()
const handleGlobalMouseMove = (event: MouseEvent) => { const handleGlobalMouseMove = (event: MouseEvent) => {
if (!draggedItem || draggedItem.stepId !== step.id) return if (!draggedItem || draggedItem.blockId !== block.id) return
const { clientX, clientY } = event const { clientX, clientY } = event
setPosition({ setPosition({
...position, ...position,
@ -55,44 +55,44 @@ export const ItemNodesList = ({
useEventListener('mousemove', handleGlobalMouseMove) useEventListener('mousemove', handleGlobalMouseMove)
useEffect(() => { useEffect(() => {
if (mouseOverBlock?.id !== step.blockId) if (mouseOverGroup?.id !== block.groupId)
setExpandedPlaceholderIndex(undefined) setExpandedPlaceholderIndex(undefined)
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [mouseOverBlock?.id]) }, [mouseOverGroup?.id])
const handleMouseMoveOnBlock = (event: MouseEvent) => { const handleMouseMoveOnGroup = (event: MouseEvent) => {
if (!isDraggingOnCurrentBlock || isReadOnly) return if (!isDraggingOnCurrentGroup || isReadOnly) return
const index = computeNearestPlaceholderIndex(event.pageY, placeholderRefs) const index = computeNearestPlaceholderIndex(event.pageY, placeholderRefs)
setExpandedPlaceholderIndex(index) setExpandedPlaceholderIndex(index)
} }
useEventListener( useEventListener(
'mousemove', 'mousemove',
handleMouseMoveOnBlock, handleMouseMoveOnGroup,
mouseOverBlock?.ref.current mouseOverGroup?.ref.current
) )
const handleMouseUpOnBlock = (e: MouseEvent) => { const handleMouseUpOnGroup = (e: MouseEvent) => {
setExpandedPlaceholderIndex(undefined) setExpandedPlaceholderIndex(undefined)
if (!isDraggingOnCurrentBlock) return if (!isDraggingOnCurrentGroup) return
const itemIndex = computeNearestPlaceholderIndex(e.pageY, placeholderRefs) const itemIndex = computeNearestPlaceholderIndex(e.pageY, placeholderRefs)
e.stopPropagation() e.stopPropagation()
setDraggedItem(undefined) setDraggedItem(undefined)
createItem(draggedItem as ButtonItem, { createItem(draggedItem as ButtonItem, {
groupIndex,
blockIndex, blockIndex,
stepIndex,
itemIndex, itemIndex,
}) })
} }
useEventListener( useEventListener(
'mouseup', 'mouseup',
handleMouseUpOnBlock, handleMouseUpOnGroup,
mouseOverBlock?.ref.current, mouseOverGroup?.ref.current,
{ {
capture: true, capture: true,
} }
) )
const handleStepMouseDown = const handleBlockMouseDown =
(itemIndex: number) => (itemIndex: number) =>
( (
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates }, { absolute, relative }: { absolute: Coordinates; relative: Coordinates },
@ -100,7 +100,7 @@ export const ItemNodesList = ({
) => { ) => {
if (!typebot || isReadOnly) return if (!typebot || isReadOnly) return
placeholderRefs.current.splice(itemIndex + 1, 1) placeholderRefs.current.splice(itemIndex + 1, 1)
detachItemFromStep({ blockIndex, stepIndex, itemIndex }) detachItemFromBlock({ groupIndex, blockIndex, itemIndex })
setPosition(absolute) setPosition(absolute)
setRelativeCoordinates(relative) setRelativeCoordinates(relative)
setDraggedItem(item) setDraggedItem(item)
@ -129,12 +129,12 @@ export const ItemNodesList = ({
rounded="lg" rounded="lg"
transition={showPlaceholders ? 'height 200ms' : 'none'} transition={showPlaceholders ? 'height 200ms' : 'none'}
/> />
{step.items.map((item, idx) => ( {block.items.map((item, idx) => (
<Stack key={item.id} spacing={1}> <Stack key={item.id} spacing={1}>
<ItemNode <ItemNode
item={item} item={item}
indices={{ blockIndex, stepIndex, itemIndex: idx }} indices={{ groupIndex, blockIndex, itemIndex: idx }}
onMouseDown={handleStepMouseDown(idx)} onMouseDown={handleBlockMouseDown(idx)}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
/> />
<Flex <Flex
@ -151,7 +151,7 @@ export const ItemNodesList = ({
/> />
</Stack> </Stack>
))} ))}
{isLastStep && ( {isLastBlock && (
<Flex <Flex
px="4" px="4"
py="2" py="2"
@ -166,8 +166,8 @@ export const ItemNodesList = ({
<Text color={isReadOnly ? 'inherit' : 'gray.500'}>Default</Text> <Text color={isReadOnly ? 'inherit' : 'gray.500'}>Default</Text>
<SourceEndpoint <SourceEndpoint
source={{ source={{
blockId: step.blockId, groupId: block.groupId,
stepId: step.id, blockId: block.id,
}} }}
pos="absolute" pos="absolute"
right="-49px" right="-49px"
@ -175,7 +175,7 @@ export const ItemNodesList = ({
</Flex> </Flex>
)} )}
{draggedItem && draggedItem.stepId === step.id && ( {draggedItem && draggedItem.blockId === block.id && (
<Portal> <Portal>
<ItemNodeOverlay <ItemNodeOverlay
item={draggedItem} item={draggedItem}

View File

@ -1,41 +0,0 @@
import { Input } from '@chakra-ui/react'
import { SearchableDropdown } from 'components/shared/SearchableDropdown'
import { Block } from 'models'
import { useMemo } from 'react'
import { byId } from 'utils'
type Props = {
blocks: Block[]
blockId?: string
onBlockIdSelected: (blockId: string) => void
isLoading?: boolean
}
export const BlocksDropdown = ({
blocks,
blockId,
onBlockIdSelected,
isLoading,
}: Props) => {
const currentBlock = useMemo(
() => blocks?.find(byId(blockId)),
[blockId, blocks]
)
const handleBlockSelect = (title: string) => {
const id = blocks?.find((b) => b.title === title)?.id
if (id) onBlockIdSelected(id)
}
if (isLoading) return <Input value="Loading..." isDisabled />
if (!blocks || blocks.length === 0)
return <Input value="No blocks found" isDisabled />
return (
<SearchableDropdown
selectedItem={currentBlock?.title}
items={(blocks ?? []).map((b) => b.title)}
onValueChange={handleBlockSelect}
placeholder={'Select a block'}
/>
)
}

Some files were not shown because too many files have changed in this diff Show More