refactor: ♻️ Rename step to block
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@ -22,3 +22,5 @@ firebaseServiceAccount.json
|
||||
# Wordpress
|
||||
.svn
|
||||
tags
|
||||
|
||||
dump.sql
|
||||
|
@ -1,25 +1,25 @@
|
||||
import { Flex, HStack, StackProps, Text, Tooltip } from '@chakra-ui/react'
|
||||
import { StepType, DraggableStepType } from 'models'
|
||||
import { useStepDnd } from 'contexts/GraphDndContext'
|
||||
import { BlockType, DraggableBlockType } from 'models'
|
||||
import { useBlockDnd } from 'contexts/GraphDndContext'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { StepIcon } from './StepIcon'
|
||||
import { StepTypeLabel } from './StepTypeLabel'
|
||||
import { BlockIcon } from './BlockIcon'
|
||||
import { BlockTypeLabel } from './BlockTypeLabel'
|
||||
|
||||
export const StepCard = ({
|
||||
export const BlockCard = ({
|
||||
type,
|
||||
onMouseDown,
|
||||
isDisabled = false,
|
||||
}: {
|
||||
type: DraggableStepType
|
||||
type: DraggableBlockType
|
||||
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)
|
||||
|
||||
useEffect(() => {
|
||||
setIsMouseDown(draggedStepType === type)
|
||||
}, [draggedStepType, type])
|
||||
setIsMouseDown(draggedBlockType === type)
|
||||
}, [draggedBlockType, type])
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => onMouseDown(e, type)
|
||||
|
||||
@ -43,8 +43,8 @@ export const StepCard = ({
|
||||
>
|
||||
{!isMouseDown ? (
|
||||
<>
|
||||
<StepIcon type={type} />
|
||||
<StepTypeLabel type={type} />
|
||||
<BlockIcon type={type} />
|
||||
<BlockTypeLabel type={type} />
|
||||
</>
|
||||
) : (
|
||||
<Text color="white" userSelect="none">
|
||||
@ -57,10 +57,10 @@ export const StepCard = ({
|
||||
)
|
||||
}
|
||||
|
||||
export const StepCardOverlay = ({
|
||||
export const BlockCardOverlay = ({
|
||||
type,
|
||||
...props
|
||||
}: StackProps & { type: StepType }) => {
|
||||
}: StackProps & { type: BlockType }) => {
|
||||
return (
|
||||
<HStack
|
||||
borderWidth="1px"
|
||||
@ -76,8 +76,8 @@ export const StepCardOverlay = ({
|
||||
zIndex={2}
|
||||
{...props}
|
||||
>
|
||||
<StepIcon type={type} />
|
||||
<StepTypeLabel type={type} />
|
||||
<BlockIcon type={type} />
|
||||
<BlockTypeLabel type={type} />
|
||||
</HStack>
|
||||
)
|
||||
}
|
@ -30,67 +30,67 @@ import {
|
||||
ZapierLogo,
|
||||
} from 'assets/logos'
|
||||
import {
|
||||
BubbleStepType,
|
||||
InputStepType,
|
||||
IntegrationStepType,
|
||||
LogicStepType,
|
||||
StepType,
|
||||
BubbleBlockType,
|
||||
InputBlockType,
|
||||
IntegrationBlockType,
|
||||
LogicBlockType,
|
||||
BlockType,
|
||||
} from 'models'
|
||||
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) {
|
||||
case BubbleStepType.TEXT:
|
||||
case BubbleBlockType.TEXT:
|
||||
return <ChatIcon color="blue.500" {...props} />
|
||||
case BubbleStepType.IMAGE:
|
||||
case BubbleBlockType.IMAGE:
|
||||
return <ImageIcon color="blue.500" {...props} />
|
||||
case BubbleStepType.VIDEO:
|
||||
case BubbleBlockType.VIDEO:
|
||||
return <FilmIcon color="blue.500" {...props} />
|
||||
case BubbleStepType.EMBED:
|
||||
case BubbleBlockType.EMBED:
|
||||
return <LayoutIcon color="blue.500" {...props} />
|
||||
case InputStepType.TEXT:
|
||||
case InputBlockType.TEXT:
|
||||
return <TextIcon color="orange.500" {...props} />
|
||||
case InputStepType.NUMBER:
|
||||
case InputBlockType.NUMBER:
|
||||
return <NumberIcon color="orange.500" {...props} />
|
||||
case InputStepType.EMAIL:
|
||||
case InputBlockType.EMAIL:
|
||||
return <EmailIcon color="orange.500" {...props} />
|
||||
case InputStepType.URL:
|
||||
case InputBlockType.URL:
|
||||
return <GlobeIcon color="orange.500" {...props} />
|
||||
case InputStepType.DATE:
|
||||
case InputBlockType.DATE:
|
||||
return <CalendarIcon color="orange.500" {...props} />
|
||||
case InputStepType.PHONE:
|
||||
case InputBlockType.PHONE:
|
||||
return <PhoneIcon color="orange.500" {...props} />
|
||||
case InputStepType.CHOICE:
|
||||
case InputBlockType.CHOICE:
|
||||
return <CheckSquareIcon color="orange.500" {...props} />
|
||||
case InputStepType.PAYMENT:
|
||||
case InputBlockType.PAYMENT:
|
||||
return <CreditCardIcon color="orange.500" {...props} />
|
||||
case InputStepType.RATING:
|
||||
case InputBlockType.RATING:
|
||||
return <StarIcon color="orange.500" {...props} />
|
||||
case LogicStepType.SET_VARIABLE:
|
||||
case LogicBlockType.SET_VARIABLE:
|
||||
return <EditIcon color="purple.500" {...props} />
|
||||
case LogicStepType.CONDITION:
|
||||
case LogicBlockType.CONDITION:
|
||||
return <FilterIcon color="purple.500" {...props} />
|
||||
case LogicStepType.REDIRECT:
|
||||
case LogicBlockType.REDIRECT:
|
||||
return <ExternalLinkIcon color="purple.500" {...props} />
|
||||
case LogicStepType.CODE:
|
||||
case LogicBlockType.CODE:
|
||||
return <CodeIcon color="purple.500" {...props} />
|
||||
case LogicStepType.TYPEBOT_LINK:
|
||||
case LogicBlockType.TYPEBOT_LINK:
|
||||
return <BoxIcon color="purple.500" {...props} />
|
||||
case IntegrationStepType.GOOGLE_SHEETS:
|
||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||
return <GoogleSheetsLogo {...props} />
|
||||
case IntegrationStepType.GOOGLE_ANALYTICS:
|
||||
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
||||
return <GoogleAnalyticsLogo {...props} />
|
||||
case IntegrationStepType.WEBHOOK:
|
||||
case IntegrationBlockType.WEBHOOK:
|
||||
return <WebhookIcon {...props} />
|
||||
case IntegrationStepType.ZAPIER:
|
||||
case IntegrationBlockType.ZAPIER:
|
||||
return <ZapierLogo {...props} />
|
||||
case IntegrationStepType.MAKE_COM:
|
||||
case IntegrationBlockType.MAKE_COM:
|
||||
return <MakeComLogo {...props} />
|
||||
case IntegrationStepType.PABBLY_CONNECT:
|
||||
case IntegrationBlockType.PABBLY_CONNECT:
|
||||
return <PabblyConnectLogo {...props} />
|
||||
case IntegrationStepType.EMAIL:
|
||||
case IntegrationBlockType.EMAIL:
|
||||
return <SendEmailIcon {...props} />
|
||||
case 'start':
|
||||
return <FlagIcon {...props} />
|
@ -1,87 +1,87 @@
|
||||
import { Text, Tooltip } from '@chakra-ui/react'
|
||||
import {
|
||||
BubbleStepType,
|
||||
InputStepType,
|
||||
IntegrationStepType,
|
||||
LogicStepType,
|
||||
StepType,
|
||||
BubbleBlockType,
|
||||
InputBlockType,
|
||||
IntegrationBlockType,
|
||||
LogicBlockType,
|
||||
BlockType,
|
||||
} from 'models'
|
||||
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) {
|
||||
case 'start':
|
||||
return <Text>Start</Text>
|
||||
case BubbleStepType.TEXT:
|
||||
case InputStepType.TEXT:
|
||||
case BubbleBlockType.TEXT:
|
||||
case InputBlockType.TEXT:
|
||||
return <Text>Text</Text>
|
||||
case BubbleStepType.IMAGE:
|
||||
case BubbleBlockType.IMAGE:
|
||||
return <Text>Image</Text>
|
||||
case BubbleStepType.VIDEO:
|
||||
case BubbleBlockType.VIDEO:
|
||||
return <Text>Video</Text>
|
||||
case BubbleStepType.EMBED:
|
||||
case BubbleBlockType.EMBED:
|
||||
return (
|
||||
<Tooltip label="Embed a pdf, an iframe, a website...">
|
||||
<Text>Embed</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
case InputStepType.NUMBER:
|
||||
case InputBlockType.NUMBER:
|
||||
return <Text>Number</Text>
|
||||
case InputStepType.EMAIL:
|
||||
case InputBlockType.EMAIL:
|
||||
return <Text>Email</Text>
|
||||
case InputStepType.URL:
|
||||
case InputBlockType.URL:
|
||||
return <Text>Website</Text>
|
||||
case InputStepType.DATE:
|
||||
case InputBlockType.DATE:
|
||||
return <Text>Date</Text>
|
||||
case InputStepType.PHONE:
|
||||
case InputBlockType.PHONE:
|
||||
return <Text>Phone</Text>
|
||||
case InputStepType.CHOICE:
|
||||
case InputBlockType.CHOICE:
|
||||
return <Text>Button</Text>
|
||||
case InputStepType.PAYMENT:
|
||||
case InputBlockType.PAYMENT:
|
||||
return <Text>Payment</Text>
|
||||
case InputStepType.RATING:
|
||||
case InputBlockType.RATING:
|
||||
return <Text>Rating</Text>
|
||||
case LogicStepType.SET_VARIABLE:
|
||||
case LogicBlockType.SET_VARIABLE:
|
||||
return <Text>Set variable</Text>
|
||||
case LogicStepType.CONDITION:
|
||||
case LogicBlockType.CONDITION:
|
||||
return <Text>Condition</Text>
|
||||
case LogicStepType.REDIRECT:
|
||||
case LogicBlockType.REDIRECT:
|
||||
return <Text>Redirect</Text>
|
||||
case LogicStepType.CODE:
|
||||
case LogicBlockType.CODE:
|
||||
return (
|
||||
<Tooltip label="Run Javascript code">
|
||||
<Text>Code</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
case LogicStepType.TYPEBOT_LINK:
|
||||
case LogicBlockType.TYPEBOT_LINK:
|
||||
return (
|
||||
<Tooltip label="Link to another of your typebots">
|
||||
<Text>Typebot</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
case IntegrationStepType.GOOGLE_SHEETS:
|
||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||
return (
|
||||
<Tooltip label="Google Sheets">
|
||||
<Text>Sheets</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
case IntegrationStepType.GOOGLE_ANALYTICS:
|
||||
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
||||
return (
|
||||
<Tooltip label="Google Analytics">
|
||||
<Text>Analytics</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
case IntegrationStepType.WEBHOOK:
|
||||
case IntegrationBlockType.WEBHOOK:
|
||||
return <Text>Webhook</Text>
|
||||
case IntegrationStepType.ZAPIER:
|
||||
case IntegrationBlockType.ZAPIER:
|
||||
return <Text>Zapier</Text>
|
||||
case IntegrationStepType.MAKE_COM:
|
||||
case IntegrationBlockType.MAKE_COM:
|
||||
return <Text>Make.com</Text>
|
||||
case IntegrationStepType.PABBLY_CONNECT:
|
||||
case IntegrationBlockType.PABBLY_CONNECT:
|
||||
return <Text>Pabbly</Text>
|
||||
case IntegrationStepType.EMAIL:
|
||||
case IntegrationBlockType.EMAIL:
|
||||
return <Text>Email</Text>
|
||||
}
|
||||
}
|
@ -10,20 +10,20 @@ import {
|
||||
Fade,
|
||||
} from '@chakra-ui/react'
|
||||
import {
|
||||
BubbleStepType,
|
||||
DraggableStepType,
|
||||
InputStepType,
|
||||
IntegrationStepType,
|
||||
LogicStepType,
|
||||
BubbleBlockType,
|
||||
DraggableBlockType,
|
||||
InputBlockType,
|
||||
IntegrationBlockType,
|
||||
LogicBlockType,
|
||||
} from 'models'
|
||||
import { useStepDnd } from 'contexts/GraphDndContext'
|
||||
import { useBlockDnd } from 'contexts/GraphDndContext'
|
||||
import React, { useState } from 'react'
|
||||
import { StepCard, StepCardOverlay } from './StepCard'
|
||||
import { BlockCard, BlockCardOverlay } from './BlockCard'
|
||||
import { LockedIcon, UnlockedIcon } from 'assets/icons'
|
||||
import { headerHeight } from 'components/shared/TypebotHeader'
|
||||
|
||||
export const StepsSideBar = () => {
|
||||
const { setDraggedStepType, draggedStepType } = useStepDnd()
|
||||
export const BlocksSideBar = () => {
|
||||
const { setDraggedBlockType, draggedBlockType } = useBlockDnd()
|
||||
const [position, setPosition] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
@ -33,7 +33,7 @@ export const StepsSideBar = () => {
|
||||
const [isExtended, setIsExtended] = useState(true)
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (!draggedStepType) return
|
||||
if (!draggedBlockType) return
|
||||
const { clientX, clientY } = event
|
||||
setPosition({
|
||||
...position,
|
||||
@ -43,19 +43,19 @@ export const StepsSideBar = () => {
|
||||
}
|
||||
useEventListener('mousemove', handleMouseMove)
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent, type: DraggableStepType) => {
|
||||
const handleMouseDown = (e: React.MouseEvent, type: DraggableBlockType) => {
|
||||
const element = e.currentTarget as HTMLDivElement
|
||||
const rect = element.getBoundingClientRect()
|
||||
setPosition({ x: rect.left, y: rect.top })
|
||||
const x = e.clientX - rect.left
|
||||
const y = e.clientY - rect.top
|
||||
setRelativeCoordinates({ x, y })
|
||||
setDraggedStepType(type)
|
||||
setDraggedBlockType(type)
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (!draggedStepType) return
|
||||
setDraggedStepType(undefined)
|
||||
if (!draggedBlockType) return
|
||||
setDraggedBlockType(undefined)
|
||||
setPosition({
|
||||
x: 0,
|
||||
y: 0,
|
||||
@ -116,8 +116,8 @@ export const StepsSideBar = () => {
|
||||
Bubbles
|
||||
</Text>
|
||||
<SimpleGrid columns={2} spacing="3">
|
||||
{Object.values(BubbleStepType).map((type) => (
|
||||
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
|
||||
{Object.values(BubbleBlockType).map((type) => (
|
||||
<BlockCard key={type} type={type} onMouseDown={handleMouseDown} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
@ -127,8 +127,8 @@ export const StepsSideBar = () => {
|
||||
Inputs
|
||||
</Text>
|
||||
<SimpleGrid columns={2} spacing="3">
|
||||
{Object.values(InputStepType).map((type) => (
|
||||
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
|
||||
{Object.values(InputBlockType).map((type) => (
|
||||
<BlockCard key={type} type={type} onMouseDown={handleMouseDown} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
@ -138,8 +138,8 @@ export const StepsSideBar = () => {
|
||||
Logic
|
||||
</Text>
|
||||
<SimpleGrid columns={2} spacing="3">
|
||||
{Object.values(LogicStepType).map((type) => (
|
||||
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
|
||||
{Object.values(LogicBlockType).map((type) => (
|
||||
<BlockCard key={type} type={type} onMouseDown={handleMouseDown} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
@ -149,21 +149,21 @@ export const StepsSideBar = () => {
|
||||
Integrations
|
||||
</Text>
|
||||
<SimpleGrid columns={2} spacing="3">
|
||||
{Object.values(IntegrationStepType).map((type) => (
|
||||
<StepCard
|
||||
{Object.values(IntegrationBlockType).map((type) => (
|
||||
<BlockCard
|
||||
key={type}
|
||||
type={type}
|
||||
onMouseDown={handleMouseDown}
|
||||
isDisabled={type === IntegrationStepType.MAKE_COM}
|
||||
isDisabled={type === IntegrationBlockType.MAKE_COM}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
|
||||
{draggedStepType && (
|
||||
{draggedBlockType && (
|
||||
<Portal>
|
||||
<StepCardOverlay
|
||||
type={draggedStepType}
|
||||
<BlockCardOverlay
|
||||
type={draggedBlockType}
|
||||
onMouseUp={handleMouseUp}
|
||||
pos="fixed"
|
||||
top="0"
|
1
apps/builder/components/editor/BlocksSideBar/index.tsx
Normal file
1
apps/builder/components/editor/BlocksSideBar/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { BlocksSideBar } from './BlocksSideBar'
|
@ -1 +0,0 @@
|
||||
export { StepsSideBar } from './StepSideBar'
|
@ -21,7 +21,7 @@ import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
|
||||
|
||||
export const PreviewDrawer = () => {
|
||||
const { typebot } = useTypebot()
|
||||
const { setRightPanel, startPreviewAtBlock } = useEditor()
|
||||
const { setRightPanel, startPreviewAtGroup } = useEditor()
|
||||
const { setPreviewingEdge } = useGraph()
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
const [width, setWidth] = useState(500)
|
||||
@ -96,14 +96,14 @@ export const PreviewDrawer = () => {
|
||||
borderRadius={'lg'}
|
||||
h="full"
|
||||
w="full"
|
||||
key={restartKey + (startPreviewAtBlock ?? '')}
|
||||
key={restartKey + (startPreviewAtGroup ?? '')}
|
||||
pointerEvents={isResizing ? 'none' : 'auto'}
|
||||
>
|
||||
<TypebotViewer
|
||||
typebot={publicTypebot}
|
||||
onNewBlockVisible={setPreviewingEdge}
|
||||
onNewGroupVisible={setPreviewingEdge}
|
||||
onNewLog={handleNewLog}
|
||||
startBlockId={startPreviewAtBlock}
|
||||
startGroupId={startPreviewAtGroup}
|
||||
isPreview
|
||||
/>
|
||||
</Flex>
|
||||
|
@ -9,12 +9,12 @@ import {
|
||||
FormLabel,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
Switch,
|
||||
Text,
|
||||
Image,
|
||||
NumberInputStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
} from '@chakra-ui/react'
|
||||
import { ColorPicker } from 'components/theme/GeneralSettings/ColorPicker'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
|
@ -5,11 +5,10 @@ import {
|
||||
Heading,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
Switch,
|
||||
HStack,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
} from '@chakra-ui/react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { PopupParams } from 'typebot-js'
|
||||
@ -55,10 +54,10 @@ export const PopupEmbedSettings = ({
|
||||
min={0}
|
||||
>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberIncrementStepper>
|
||||
</NumberInput>
|
||||
)}
|
||||
</Flex>
|
||||
|
@ -17,22 +17,22 @@ export const DrawingEdge = () => {
|
||||
connectingIds,
|
||||
sourceEndpoints,
|
||||
targetEndpoints,
|
||||
blocksCoordinates,
|
||||
groupsCoordinates,
|
||||
} = useGraph()
|
||||
const { createEdge } = useTypebot()
|
||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
|
||||
|
||||
const sourceBlockCoordinates =
|
||||
blocksCoordinates && blocksCoordinates[connectingIds?.source.blockId ?? '']
|
||||
const targetBlockCoordinates =
|
||||
blocksCoordinates && blocksCoordinates[connectingIds?.target?.blockId ?? '']
|
||||
const sourceGroupCoordinates =
|
||||
groupsCoordinates && groupsCoordinates[connectingIds?.source.groupId ?? '']
|
||||
const targetGroupCoordinates =
|
||||
groupsCoordinates && groupsCoordinates[connectingIds?.target?.groupId ?? '']
|
||||
|
||||
const sourceTop = useMemo(() => {
|
||||
if (!connectingIds) return 0
|
||||
return getEndpointTopOffset({
|
||||
endpoints: sourceEndpoints,
|
||||
graphOffsetY: graphPosition.y,
|
||||
endpointId: connectingIds.source.itemId ?? connectingIds.source.stepId,
|
||||
endpointId: connectingIds.source.itemId ?? connectingIds.source.blockId,
|
||||
graphScale: graphPosition.scale,
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -43,32 +43,32 @@ export const DrawingEdge = () => {
|
||||
return getEndpointTopOffset({
|
||||
endpoints: targetEndpoints,
|
||||
graphOffsetY: graphPosition.y,
|
||||
endpointId: connectingIds.target?.stepId,
|
||||
endpointId: connectingIds.target?.blockId,
|
||||
graphScale: graphPosition.scale,
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [connectingIds, targetEndpoints])
|
||||
|
||||
const path = useMemo(() => {
|
||||
if (!sourceTop || !sourceBlockCoordinates) return ``
|
||||
if (!sourceTop || !sourceGroupCoordinates) return ``
|
||||
|
||||
return targetBlockCoordinates
|
||||
return targetGroupCoordinates
|
||||
? computeConnectingEdgePath({
|
||||
sourceBlockCoordinates,
|
||||
targetBlockCoordinates,
|
||||
sourceGroupCoordinates,
|
||||
targetGroupCoordinates,
|
||||
sourceTop,
|
||||
targetTop,
|
||||
graphScale: graphPosition.scale,
|
||||
})
|
||||
: computeEdgePathToMouse({
|
||||
sourceBlockCoordinates,
|
||||
sourceGroupCoordinates,
|
||||
mousePosition,
|
||||
sourceTop,
|
||||
})
|
||||
}, [
|
||||
sourceTop,
|
||||
sourceBlockCoordinates,
|
||||
targetBlockCoordinates,
|
||||
sourceGroupCoordinates,
|
||||
targetGroupCoordinates,
|
||||
targetTop,
|
||||
mousePosition,
|
||||
graphPosition.scale,
|
||||
|
@ -13,37 +13,37 @@ import { isFreePlan } from 'services/workspace'
|
||||
import { byId, isDefined } from 'utils'
|
||||
|
||||
type Props = {
|
||||
blockId: string
|
||||
groupId: string
|
||||
answersCounts: AnswersCount[]
|
||||
onUnlockProPlanClick?: () => void
|
||||
}
|
||||
|
||||
export const DropOffEdge = ({
|
||||
answersCounts,
|
||||
blockId,
|
||||
groupId,
|
||||
onUnlockProPlanClick,
|
||||
}: Props) => {
|
||||
const { workspace } = useWorkspace()
|
||||
const { sourceEndpoints, blocksCoordinates, graphPosition } = useGraph()
|
||||
const { sourceEndpoints, groupsCoordinates, graphPosition } = useGraph()
|
||||
const { publishedTypebot } = useTypebot()
|
||||
|
||||
const isUserOnFreePlan = isFreePlan(workspace)
|
||||
|
||||
const totalAnswers = useMemo(
|
||||
() => answersCounts.find((a) => a.blockId === blockId)?.totalAnswers,
|
||||
[answersCounts, blockId]
|
||||
() => answersCounts.find((a) => a.groupId === groupId)?.totalAnswers,
|
||||
[answersCounts, groupId]
|
||||
)
|
||||
|
||||
const { totalDroppedUser, dropOffRate } = useMemo(() => {
|
||||
if (!publishedTypebot || totalAnswers === undefined)
|
||||
return { previousTotal: undefined, dropOffRate: undefined }
|
||||
const previousBlockIds = publishedTypebot.edges
|
||||
const previousGroupIds = publishedTypebot.edges
|
||||
.map((edge) =>
|
||||
edge.to.blockId === blockId ? edge.from.blockId : undefined
|
||||
edge.to.groupId === groupId ? edge.from.groupId : undefined
|
||||
)
|
||||
.filter(isDefined)
|
||||
const previousTotal = answersCounts
|
||||
.filter((a) => previousBlockIds.includes(a.blockId))
|
||||
.filter((a) => previousGroupIds.includes(a.groupId))
|
||||
.reduce((prev, acc) => acc.totalAnswers + prev, 0)
|
||||
if (previousTotal === 0)
|
||||
return { previousTotal: undefined, dropOffRate: undefined }
|
||||
@ -53,25 +53,25 @@ export const DropOffEdge = ({
|
||||
totalDroppedUser,
|
||||
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(
|
||||
() =>
|
||||
getEndpointTopOffset({
|
||||
endpoints: sourceEndpoints,
|
||||
graphOffsetY: graphPosition.y,
|
||||
endpointId: block?.steps[block.steps.length - 1].id,
|
||||
endpointId: group?.blocks[group.blocks.length - 1].id,
|
||||
graphScale: graphPosition.scale,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[block?.steps, sourceEndpoints, blocksCoordinates]
|
||||
[group?.blocks, sourceEndpoints, groupsCoordinates]
|
||||
)
|
||||
|
||||
const labelCoordinates = useMemo(() => {
|
||||
if (!blocksCoordinates[blockId]) return
|
||||
return computeSourceCoordinates(blocksCoordinates[blockId], sourceTop ?? 0)
|
||||
}, [blocksCoordinates, blockId, sourceTop])
|
||||
if (!groupsCoordinates[groupId]) return
|
||||
return computeSourceCoordinates(groupsCoordinates[groupId], sourceTop ?? 0)
|
||||
}, [groupsCoordinates, groupId, sourceTop])
|
||||
|
||||
if (!labelCoordinates) return <></>
|
||||
return (
|
||||
|
@ -25,7 +25,7 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
|
||||
previewingEdge,
|
||||
sourceEndpoints,
|
||||
targetEndpoints,
|
||||
blocksCoordinates,
|
||||
groupsCoordinates,
|
||||
graphPosition,
|
||||
isReadOnly,
|
||||
setPreviewingEdge,
|
||||
@ -37,10 +37,10 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
|
||||
|
||||
const isPreviewing = isMouseOver || previewingEdge?.id === edge.id
|
||||
|
||||
const sourceBlockCoordinates =
|
||||
blocksCoordinates && blocksCoordinates[edge.from.blockId]
|
||||
const targetBlockCoordinates =
|
||||
blocksCoordinates && blocksCoordinates[edge.to.blockId]
|
||||
const sourceGroupCoordinates =
|
||||
groupsCoordinates && groupsCoordinates[edge.from.groupId]
|
||||
const targetGroupCoordinates =
|
||||
groupsCoordinates && groupsCoordinates[edge.to.groupId]
|
||||
|
||||
const sourceTop = useMemo(
|
||||
() =>
|
||||
@ -51,7 +51,7 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
|
||||
graphScale: graphPosition.scale,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[sourceBlockCoordinates?.y, edge, sourceEndpoints, refreshEdge]
|
||||
[sourceGroupCoordinates?.y, edge, sourceEndpoints, refreshEdge]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@ -62,7 +62,7 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
|
||||
getEndpointTopOffset({
|
||||
endpoints: targetEndpoints,
|
||||
graphOffsetY: graphPosition.y,
|
||||
endpointId: edge?.to.stepId,
|
||||
endpointId: edge?.to.blockId,
|
||||
graphScale: graphPosition.scale,
|
||||
})
|
||||
)
|
||||
@ -71,24 +71,24 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
|
||||
getEndpointTopOffset({
|
||||
endpoints: targetEndpoints,
|
||||
graphOffsetY: graphPosition.y,
|
||||
endpointId: edge?.to.stepId,
|
||||
endpointId: edge?.to.blockId,
|
||||
graphScale: graphPosition.scale,
|
||||
})
|
||||
)
|
||||
}, [
|
||||
targetBlockCoordinates?.y,
|
||||
targetGroupCoordinates?.y,
|
||||
targetEndpoints,
|
||||
graphPosition.y,
|
||||
edge?.to.stepId,
|
||||
edge?.to.blockId,
|
||||
graphPosition.scale,
|
||||
])
|
||||
|
||||
const path = useMemo(() => {
|
||||
if (!sourceBlockCoordinates || !targetBlockCoordinates || !sourceTop)
|
||||
if (!sourceGroupCoordinates || !targetGroupCoordinates || !sourceTop)
|
||||
return ``
|
||||
const anchorsPosition = getAnchorsPosition({
|
||||
sourceBlockCoordinates,
|
||||
targetBlockCoordinates,
|
||||
sourceGroupCoordinates,
|
||||
targetGroupCoordinates,
|
||||
sourceTop,
|
||||
targetTop,
|
||||
graphScale: graphPosition.scale,
|
||||
@ -96,10 +96,10 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
|
||||
return computeEdgePath(anchorsPosition)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
sourceBlockCoordinates?.x,
|
||||
sourceBlockCoordinates?.y,
|
||||
targetBlockCoordinates?.x,
|
||||
targetBlockCoordinates?.y,
|
||||
sourceGroupCoordinates?.x,
|
||||
sourceGroupCoordinates?.y,
|
||||
targetGroupCoordinates?.x,
|
||||
targetGroupCoordinates?.y,
|
||||
sourceTop,
|
||||
])
|
||||
|
||||
|
@ -34,9 +34,9 @@ export const Edges = ({
|
||||
))}
|
||||
{answersCounts?.slice(1)?.map((answerCount) => (
|
||||
<DropOffEdge
|
||||
key={answerCount.blockId}
|
||||
key={answerCount.groupId}
|
||||
answersCounts={answersCounts}
|
||||
blockId={answerCount.blockId}
|
||||
groupId={answerCount.groupId}
|
||||
onUnlockProPlanClick={onUnlockProPlanClick}
|
||||
/>
|
||||
))}
|
||||
|
@ -13,7 +13,7 @@ export const SourceEndpoint = ({
|
||||
const {
|
||||
setConnectingIds,
|
||||
addSourceEndpoint,
|
||||
blocksCoordinates,
|
||||
groupsCoordinates,
|
||||
previewingEdge,
|
||||
} = useGraph()
|
||||
const ref = useRef<HTMLDivElement | null>(null)
|
||||
@ -24,18 +24,18 @@ export const SourceEndpoint = ({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (ranOnce || !ref.current || Object.keys(blocksCoordinates).length === 0)
|
||||
if (ranOnce || !ref.current || Object.keys(groupsCoordinates).length === 0)
|
||||
return
|
||||
const id = source.itemId ?? source.stepId
|
||||
const id = source.itemId ?? source.blockId
|
||||
addSourceEndpoint({
|
||||
id,
|
||||
ref,
|
||||
})
|
||||
setRanOnce(true)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ref.current, blocksCoordinates])
|
||||
}, [ref.current, groupsCoordinates])
|
||||
|
||||
if (!blocksCoordinates) return <></>
|
||||
if (!groupsCoordinates) return <></>
|
||||
return (
|
||||
<Flex
|
||||
ref={ref}
|
||||
@ -62,7 +62,7 @@ export const SourceEndpoint = ({
|
||||
borderWidth="3.5px"
|
||||
shadow={`sm`}
|
||||
borderColor={
|
||||
previewingEdge?.from.stepId === source.stepId &&
|
||||
previewingEdge?.from.blockId === source.blockId &&
|
||||
previewingEdge.from.itemId === source.itemId
|
||||
? 'blue.300'
|
||||
: 'blue.200'
|
||||
|
@ -3,11 +3,11 @@ import { useGraph } from 'contexts/GraphContext'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
|
||||
export const TargetEndpoint = ({
|
||||
stepId,
|
||||
blockId,
|
||||
isVisible,
|
||||
...props
|
||||
}: BoxProps & {
|
||||
stepId: string
|
||||
blockId: string
|
||||
isVisible?: boolean
|
||||
}) => {
|
||||
const { addTargetEndpoint } = useGraph()
|
||||
@ -16,7 +16,7 @@ export const TargetEndpoint = ({
|
||||
useEffect(() => {
|
||||
if (!ref.current) return
|
||||
addTargetEndpoint({
|
||||
id: stepId,
|
||||
id: blockId,
|
||||
ref,
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
@ -6,9 +6,9 @@ import {
|
||||
graphPositionDefaultValue,
|
||||
useGraph,
|
||||
} from 'contexts/GraphContext'
|
||||
import { useStepDnd } from 'contexts/GraphDndContext'
|
||||
import { useBlockDnd } from 'contexts/GraphDndContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { DraggableStepType, PublicTypebot, Typebot } from 'models'
|
||||
import { DraggableBlockType, PublicTypebot, Typebot } from 'models'
|
||||
import { AnswersCount } from 'services/analytics'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'
|
||||
@ -21,7 +21,7 @@ import { ZoomButtons } from './ZoomButtons'
|
||||
|
||||
const maxScale = 1.5
|
||||
const minScale = 0.1
|
||||
const zoomButtonsScaleStep = 0.2
|
||||
const zoomButtonsScaleBlock = 0.2
|
||||
|
||||
export const Graph = ({
|
||||
typebot,
|
||||
@ -34,20 +34,20 @@ export const Graph = ({
|
||||
onUnlockProPlanClick?: () => void
|
||||
} & FlexProps) => {
|
||||
const {
|
||||
draggedStepType,
|
||||
setDraggedStepType,
|
||||
draggedStep,
|
||||
setDraggedStep,
|
||||
draggedBlockType,
|
||||
setDraggedBlockType,
|
||||
draggedBlock,
|
||||
setDraggedBlock,
|
||||
draggedItem,
|
||||
setDraggedItem,
|
||||
} = useStepDnd()
|
||||
} = useBlockDnd()
|
||||
const graphContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
const editorContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
const { createBlock } = useTypebot()
|
||||
const { createGroup } = useTypebot()
|
||||
const {
|
||||
setGraphPosition: setGlobalGraphPosition,
|
||||
setOpenedStepId,
|
||||
updateBlockCoordinates,
|
||||
setOpenedBlockId,
|
||||
updateGroupCoordinates,
|
||||
setPreviewingEdge,
|
||||
connectingIds,
|
||||
} = useGraph()
|
||||
@ -100,22 +100,22 @@ export const Graph = ({
|
||||
const handleMouseUp = (e: MouseEvent) => {
|
||||
if (!typebot) return
|
||||
if (draggedItem) setDraggedItem(undefined)
|
||||
if (!draggedStep && !draggedStepType) return
|
||||
if (!draggedBlock && !draggedBlockType) return
|
||||
|
||||
const coordinates = projectMouse(
|
||||
{ x: e.clientX, y: e.clientY },
|
||||
graphPosition
|
||||
)
|
||||
const id = cuid()
|
||||
updateBlockCoordinates(id, coordinates)
|
||||
createBlock({
|
||||
updateGroupCoordinates(id, coordinates)
|
||||
createGroup({
|
||||
id,
|
||||
...coordinates,
|
||||
step: draggedStep ?? (draggedStepType as DraggableStepType),
|
||||
indices: { blockIndex: typebot.blocks.length, stepIndex: 0 },
|
||||
block: draggedBlock ?? (draggedBlockType as DraggableBlockType),
|
||||
indices: { groupIndex: typebot.groups.length, blockIndex: 0 },
|
||||
})
|
||||
setDraggedStep(undefined)
|
||||
setDraggedStepType(undefined)
|
||||
setDraggedBlock(undefined)
|
||||
setDraggedBlockType(undefined)
|
||||
}
|
||||
|
||||
const handleCaptureMouseDown = (e: MouseEvent) => {
|
||||
@ -124,7 +124,7 @@ export const Graph = ({
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
setOpenedStepId(undefined)
|
||||
setOpenedBlockId(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 mouseY = y - headerHeight
|
||||
let scale = graphPosition.scale + delta
|
||||
@ -181,8 +181,8 @@ export const Graph = ({
|
||||
<DraggableCore onDrag={onDrag} enableUserSelectHack={false}>
|
||||
<Flex ref={graphContainerRef} position="relative" {...props}>
|
||||
<ZoomButtons
|
||||
onZoomIn={() => zoom(zoomButtonsScaleStep)}
|
||||
onZoomOut={() => zoom(-zoomButtonsScaleStep)}
|
||||
onZoomIn={() => zoom(zoomButtonsScaleBlock)}
|
||||
onZoomOut={() => zoom(-zoomButtonsScaleBlock)}
|
||||
/>
|
||||
<Flex
|
||||
flex="1"
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { Block } from 'models'
|
||||
import { Group } from 'models'
|
||||
import React from 'react'
|
||||
import { AnswersCount } from 'services/analytics'
|
||||
import { Edges } from './Edges'
|
||||
import { BlockNode } from './Nodes/BlockNode'
|
||||
import { GroupNode } from './Nodes/GroupNode'
|
||||
|
||||
type Props = {
|
||||
answersCounts?: AnswersCount[]
|
||||
@ -18,8 +18,8 @@ const MyComponent = ({ answersCounts, onUnlockProPlanClick }: Props) => {
|
||||
answersCounts={answersCounts}
|
||||
onUnlockProPlanClick={onUnlockProPlanClick}
|
||||
/>
|
||||
{typebot?.blocks.map((block, idx) => (
|
||||
<BlockNode block={block as Block} blockIndex={idx} key={block.id} />
|
||||
{typebot?.groups.map((group, idx) => (
|
||||
<GroupNode group={group as Group} groupIndex={idx} key={group.id} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
@ -1,193 +1,248 @@
|
||||
import {
|
||||
Editable,
|
||||
EditableInput,
|
||||
EditablePreview,
|
||||
IconButton,
|
||||
Stack,
|
||||
Flex,
|
||||
HStack,
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/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 { useStepDnd } from 'contexts/GraphDndContext'
|
||||
import { StepNodesList } from '../StepNode/StepNodesList'
|
||||
import { isDefined, isNotDefined } from 'utils'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { BlockIcon } from 'components/editor/BlocksSideBar/BlockIcon'
|
||||
import { isBubbleBlock, isTextBubbleBlock } from 'utils'
|
||||
import { BlockNodeContent } from './BlockNodeContent/BlockNodeContent'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { ContextMenu } from 'components/shared/ContextMenu'
|
||||
import { SettingsPopoverContent } from './SettingsPopoverContent'
|
||||
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 { 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
|
||||
blockIndex: number
|
||||
}
|
||||
|
||||
export const BlockNode = ({ block, blockIndex }: Props) => {
|
||||
isConnectable: boolean
|
||||
indices: { blockIndex: number; groupIndex: number }
|
||||
onMouseDown?: (blockNodePosition: NodePosition, block: DraggableBlock) => void
|
||||
}) => {
|
||||
const { query } = useRouter()
|
||||
const {
|
||||
connectingIds,
|
||||
setConnectingIds,
|
||||
connectingIds,
|
||||
openedBlockId,
|
||||
setOpenedBlockId,
|
||||
setFocusedGroupId,
|
||||
previewingEdge,
|
||||
blocksCoordinates,
|
||||
updateBlockCoordinates,
|
||||
isReadOnly,
|
||||
focusedBlockId,
|
||||
setFocusedBlockId,
|
||||
graphPosition,
|
||||
} = useGraph()
|
||||
const { typebot, updateBlock } = useTypebot()
|
||||
const { setMouseOverBlock, mouseOverBlock } = useStepDnd()
|
||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||
const { updateBlock } = useTypebot()
|
||||
const [isConnecting, setIsConnecting] = useState(false)
|
||||
const { setRightPanel, setStartPreviewAtBlock } = useEditor()
|
||||
const isPreviewing =
|
||||
previewingEdge?.from.blockId === block.id ||
|
||||
(previewingEdge?.to.blockId === block.id &&
|
||||
isNotDefined(previewingEdge.to.stepId))
|
||||
const isStartBlock =
|
||||
isDefined(block.steps[0]) && block.steps[0].type === 'start'
|
||||
|
||||
const blockCoordinates = blocksCoordinates[block.id]
|
||||
const [isPopoverOpened, setIsPopoverOpened] = useState(
|
||||
openedBlockId === block.id
|
||||
)
|
||||
const [isEditing, setIsEditing] = useState<boolean>(
|
||||
isTextBubbleBlock(block) && block.content.plainText === ''
|
||||
)
|
||||
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(() => {
|
||||
if (!debouncedBlockPosition || isReadOnly) return
|
||||
if (
|
||||
debouncedBlockPosition?.x === block.graphCoordinates.x &&
|
||||
debouncedBlockPosition.y === block.graphCoordinates.y
|
||||
)
|
||||
return
|
||||
updateBlock(blockIndex, { graphCoordinates: debouncedBlockPosition })
|
||||
if (query.blockId?.toString() === block.id) setOpenedBlockId(block.id)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedBlockPosition])
|
||||
}, [query])
|
||||
|
||||
useEffect(() => {
|
||||
setIsConnecting(
|
||||
connectingIds?.target?.blockId === block.id &&
|
||||
isNotDefined(connectingIds.target?.stepId)
|
||||
connectingIds?.target?.groupId === block.groupId &&
|
||||
connectingIds?.target?.blockId === block.id
|
||||
)
|
||||
}, [block.id, connectingIds])
|
||||
}, [connectingIds, block.groupId, block.id])
|
||||
|
||||
const handleTitleSubmit = (title: string) =>
|
||||
updateBlock(blockIndex, { title })
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
const handleModalClose = () => {
|
||||
updateBlock(indices, { ...block })
|
||||
onModalClose()
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (isReadOnly) return
|
||||
if (mouseOverBlock?.id !== block.id && !isStartBlock)
|
||||
setMouseOverBlock({ id: block.id, ref: blockRef })
|
||||
if (connectingIds)
|
||||
setConnectingIds({ ...connectingIds, target: { blockId: block.id } })
|
||||
setConnectingIds({
|
||||
...connectingIds,
|
||||
target: { groupId: block.groupId, blockId: block.id },
|
||||
})
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (isReadOnly) return
|
||||
setMouseOverBlock(undefined)
|
||||
if (connectingIds) setConnectingIds({ ...connectingIds, target: undefined })
|
||||
if (connectingIds?.target)
|
||||
setConnectingIds({
|
||||
...connectingIds,
|
||||
target: { ...connectingIds.target, blockId: undefined },
|
||||
})
|
||||
}
|
||||
|
||||
const onDrag = (_: DraggableEvent, draggableData: DraggableData) => {
|
||||
const { deltaX, deltaY } = draggableData
|
||||
updateBlockCoordinates(block.id, {
|
||||
x: blockCoordinates.x + deltaX / graphPosition.scale,
|
||||
y: blockCoordinates.y + deltaY / graphPosition.scale,
|
||||
})
|
||||
const handleCloseEditor = (content: TextBubbleContent) => {
|
||||
const updatedBlock = { ...block, content } as Block
|
||||
updateBlock(indices, updatedBlock)
|
||||
setIsEditing(false)
|
||||
}
|
||||
|
||||
const onDragStart = () => {
|
||||
setFocusedBlockId(block.id)
|
||||
setIsMouseDown(true)
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
setFocusedGroupId(block.groupId)
|
||||
e.stopPropagation()
|
||||
if (isTextBubbleBlock(block)) setIsEditing(true)
|
||||
setOpenedBlockId(block.id)
|
||||
}
|
||||
|
||||
const startPreviewAtThisBlock = () => {
|
||||
setStartPreviewAtBlock(block.id)
|
||||
setRightPanel(RightPanel.PREVIEW)
|
||||
const handleExpandClick = () => {
|
||||
setOpenedBlockId(undefined)
|
||||
onModalOpen()
|
||||
}
|
||||
|
||||
const onDragStop = () => setIsMouseDown(false)
|
||||
return (
|
||||
const handleBlockUpdate = (updates: Partial<Block>) =>
|
||||
updateBlock(indices, { ...block, ...updates })
|
||||
|
||||
const handleContentChange = (content: BubbleBlockContent) =>
|
||||
updateBlock(indices, { ...block, content } as Block)
|
||||
|
||||
useEffect(() => {
|
||||
setIsPopoverOpened(openedBlockId === block.id)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [openedBlockId])
|
||||
|
||||
return isEditing && isTextBubbleBlock(block) ? (
|
||||
<TextBubbleEditor
|
||||
initialValue={block.content.richText}
|
||||
onClose={handleCloseEditor}
|
||||
/>
|
||||
) : (
|
||||
<ContextMenu<HTMLDivElement>
|
||||
renderMenu={() => <BlockNodeContextMenu blockIndex={blockIndex} />}
|
||||
isDisabled={isReadOnly || isStartBlock}
|
||||
renderMenu={() => <BlockNodeContextMenu indices={indices} />}
|
||||
>
|
||||
{(ref, isOpened) => (
|
||||
<DraggableCore
|
||||
enableUserSelectHack={false}
|
||||
onDrag={onDrag}
|
||||
onStart={onDragStart}
|
||||
onStop={onDragStop}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
<Popover
|
||||
placement="left"
|
||||
isLazy
|
||||
isOpen={isPopoverOpened}
|
||||
closeOnBlur={false}
|
||||
>
|
||||
<Stack
|
||||
ref={setMultipleRefs([ref, blockRef])}
|
||||
data-testid="block"
|
||||
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(${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'}
|
||||
<PopoverTrigger>
|
||||
<Flex
|
||||
pos="relative"
|
||||
ref={setMultipleRefs([ref, blockRef])}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleClick}
|
||||
data-testid={`block`}
|
||||
w="full"
|
||||
>
|
||||
<EditablePreview
|
||||
_hover={{ bgColor: 'gray.200' }}
|
||||
px="1"
|
||||
userSelect={'none'}
|
||||
<HStack
|
||||
flex="1"
|
||||
userSelect="none"
|
||||
p="3"
|
||||
borderWidth={isOpened || isPreviewing ? '2px' : '1px'}
|
||||
borderColor={isOpened || isPreviewing ? 'blue.400' : 'gray.200'}
|
||||
margin={isOpened || isPreviewing ? '-1px' : 0}
|
||||
rounded="lg"
|
||||
cursor={'pointer'}
|
||||
bgColor="gray.50"
|
||||
align="flex-start"
|
||||
w="full"
|
||||
transition="border-color 0.2s"
|
||||
>
|
||||
<BlockIcon
|
||||
type={block.type}
|
||||
mt="1"
|
||||
data-testid={`${block.id}-icon`}
|
||||
/>
|
||||
<BlockNodeContent block={block} indices={indices} />
|
||||
<TargetEndpoint
|
||||
pos="absolute"
|
||||
left="-32px"
|
||||
top="19px"
|
||||
blockId={block.id}
|
||||
/>
|
||||
{isConnectable && hasDefaultConnector(block) && (
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
groupId: block.groupId,
|
||||
blockId: block.id,
|
||||
}}
|
||||
pos="absolute"
|
||||
right="-34px"
|
||||
bottom="10px"
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
</Flex>
|
||||
</PopoverTrigger>
|
||||
{hasSettingsPopover(block) && (
|
||||
<>
|
||||
<SettingsPopoverContent
|
||||
block={block}
|
||||
onExpandClick={handleExpandClick}
|
||||
onBlockChange={handleBlockUpdate}
|
||||
/>
|
||||
<EditableInput
|
||||
minW="0"
|
||||
px="1"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</Editable>
|
||||
{typebot && (
|
||||
<StepNodesList
|
||||
blockId={block.id}
|
||||
steps={block.steps}
|
||||
blockIndex={blockIndex}
|
||||
blockRef={ref}
|
||||
isStartBlock={isStartBlock}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
icon={<PlayIcon />}
|
||||
aria-label={'Preview bot from this group'}
|
||||
pos="absolute"
|
||||
right={2}
|
||||
top={0}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={startPreviewAtThisBlock}
|
||||
<SettingsModal isOpen={isModalOpen} onClose={handleModalClose}>
|
||||
<BlockSettings
|
||||
block={block}
|
||||
onBlockChange={handleBlockUpdate}
|
||||
/>
|
||||
</SettingsModal>
|
||||
</>
|
||||
)}
|
||||
{isMediaBubbleBlock(block) && (
|
||||
<MediaBubblePopoverContent
|
||||
block={block}
|
||||
onContentChange={handleContentChange}
|
||||
/>
|
||||
</Stack>
|
||||
</DraggableCore>
|
||||
)}
|
||||
</Popover>
|
||||
)}
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
const hasSettingsPopover = (
|
||||
block: Block
|
||||
): block is BlockWithOptions | ConditionBlock => !isBubbleBlock(block)
|
||||
|
||||
const isMediaBubbleBlock = (
|
||||
block: Block
|
||||
): block is Exclude<BubbleBlock, TextBubbleBlock> =>
|
||||
isBubbleBlock(block) && !isTextBubbleBlock(block)
|
||||
|
@ -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>
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
import { Box, Text } from '@chakra-ui/react'
|
||||
import { EmbedBubbleStep } from 'models'
|
||||
import { EmbedBubbleBlock } from 'models'
|
||||
|
||||
export const EmbedBubbleContent = ({ step }: { step: EmbedBubbleStep }) => {
|
||||
if (!step.content?.url) return <Text color="gray.500">Click to edit...</Text>
|
||||
export const EmbedBubbleContent = ({ block }: { block: EmbedBubbleBlock }) => {
|
||||
if (!block.content?.url) return <Text color="gray.500">Click to edit...</Text>
|
||||
return (
|
||||
<Box w="full" h="120px" pos="relative">
|
||||
<iframe
|
||||
id="embed-bubble-content"
|
||||
src={step.content.url}
|
||||
src={block.content.url}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
@ -1,19 +1,19 @@
|
||||
import { Tag, Text, Wrap, WrapItem } from '@chakra-ui/react'
|
||||
import { SendEmailStep } from 'models'
|
||||
import { SendEmailBlock } from 'models'
|
||||
|
||||
type Props = {
|
||||
step: SendEmailStep
|
||||
block: SendEmailBlock
|
||||
}
|
||||
|
||||
export const SendEmailContent = ({ step }: Props) => {
|
||||
if (step.options.recipients.length === 0)
|
||||
export const SendEmailContent = ({ block }: Props) => {
|
||||
if (block.options.recipients.length === 0)
|
||||
return <Text color="gray.500">Configure...</Text>
|
||||
return (
|
||||
<Wrap noOfLines={2} pr="6">
|
||||
<WrapItem>
|
||||
<Text>Send email to</Text>
|
||||
</WrapItem>
|
||||
{step.options.recipients.map((to) => (
|
||||
{block.options.recipients.map((to) => (
|
||||
<WrapItem key={to}>
|
||||
<Tag>{to}</Tag>
|
||||
</WrapItem>
|
@ -1,13 +1,13 @@
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { SetVariableStep } from 'models'
|
||||
import { SetVariableBlock } from 'models'
|
||||
import { byId } from 'utils'
|
||||
|
||||
export const SetVariableContent = ({ step }: { step: SetVariableStep }) => {
|
||||
export const SetVariableContent = ({ block }: { block: SetVariableBlock }) => {
|
||||
const { typebot } = useTypebot()
|
||||
const variableName =
|
||||
typebot?.variables.find(byId(step.options.variableId))?.name ?? ''
|
||||
const expression = step.options.expressionToEvaluate ?? ''
|
||||
typebot?.variables.find(byId(block.options.variableId))?.name ?? ''
|
||||
const expression = block.options.expressionToEvaluate ?? ''
|
||||
return (
|
||||
<Text color={'gray.500'} noOfLines={2}>
|
||||
{variableName === '' && expression === ''
|
@ -1,27 +1,27 @@
|
||||
import { Flex } from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { TextBubbleStep } from 'models'
|
||||
import { TextBubbleBlock } from 'models'
|
||||
import React from 'react'
|
||||
import { parseVariableHighlight } from 'services/utils'
|
||||
|
||||
type Props = {
|
||||
step: TextBubbleStep
|
||||
block: TextBubbleBlock
|
||||
}
|
||||
|
||||
export const TextBubbleContent = ({ step }: Props) => {
|
||||
export const TextBubbleContent = ({ block }: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
if (!typebot) return <></>
|
||||
return (
|
||||
<Flex
|
||||
w="90%"
|
||||
flexDir={'column'}
|
||||
opacity={step.content.html === '' ? '0.5' : '1'}
|
||||
opacity={block.content.html === '' ? '0.5' : '1'}
|
||||
className="slate-html-container"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
step.content.html === ''
|
||||
block.content.html === ''
|
||||
? `<p>Click to edit...</p>`
|
||||
: parseVariableHighlight(step.content.html, typebot),
|
||||
: parseVariableHighlight(block.content.html, typebot),
|
||||
}}
|
||||
/>
|
||||
)
|
@ -1,26 +1,27 @@
|
||||
import { TypebotLinkStep } from 'models'
|
||||
import { TypebotLinkBlock } from 'models'
|
||||
import React from 'react'
|
||||
import { Tag, Text } from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { byId } from 'utils'
|
||||
|
||||
type Props = {
|
||||
step: TypebotLinkStep
|
||||
block: TypebotLinkBlock
|
||||
}
|
||||
|
||||
export const TypebotLinkContent = ({ step }: Props) => {
|
||||
export const TypebotLinkContent = ({ block }: Props) => {
|
||||
const { linkedTypebots, typebot } = useTypebot()
|
||||
const isCurrentTypebot =
|
||||
typebot &&
|
||||
(step.options.typebotId === typebot.id ||
|
||||
step.options.typebotId === 'current')
|
||||
(block.options.typebotId === typebot.id ||
|
||||
block.options.typebotId === 'current')
|
||||
const linkedTypebot = isCurrentTypebot
|
||||
? typebot
|
||||
: linkedTypebots?.find(byId(step.options.typebotId))
|
||||
const blockTitle = linkedTypebot?.blocks.find(
|
||||
byId(step.options.blockId)
|
||||
: linkedTypebots?.find(byId(block.options.typebotId))
|
||||
const blockTitle = linkedTypebot?.groups.find(
|
||||
byId(block.options.groupId)
|
||||
)?.title
|
||||
if (!step.options.typebotId) return <Text color="gray.500">Configure...</Text>
|
||||
if (!block.options.typebotId)
|
||||
return <Text color="gray.500">Configure...</Text>
|
||||
return (
|
||||
<Text>
|
||||
Jump{' '}
|
@ -1,15 +1,15 @@
|
||||
import { Box, Text } from '@chakra-ui/react'
|
||||
import { VideoBubbleStep, VideoBubbleContentType } from 'models'
|
||||
import { VideoBubbleBlock, VideoBubbleContentType } from 'models'
|
||||
|
||||
export const VideoBubbleContent = ({ step }: { step: VideoBubbleStep }) => {
|
||||
if (!step.content?.url || !step.content.type)
|
||||
export const VideoBubbleContent = ({ block }: { block: VideoBubbleBlock }) => {
|
||||
if (!block.content?.url || !block.content.type)
|
||||
return <Text color="gray.500">Click to edit...</Text>
|
||||
switch (step.content.type) {
|
||||
switch (block.content.type) {
|
||||
case VideoBubbleContentType.URL:
|
||||
return (
|
||||
<Box w="full" h="120px" pos="relative">
|
||||
<video
|
||||
key={step.content.url}
|
||||
key={block.content.url}
|
||||
controls
|
||||
style={{
|
||||
width: '100%',
|
||||
@ -20,20 +20,20 @@ export const VideoBubbleContent = ({ step }: { step: VideoBubbleStep }) => {
|
||||
borderRadius: '10px',
|
||||
}}
|
||||
>
|
||||
<source src={step.content.url} />
|
||||
<source src={block.content.url} />
|
||||
</video>
|
||||
</Box>
|
||||
)
|
||||
case VideoBubbleContentType.VIMEO:
|
||||
case VideoBubbleContentType.YOUTUBE: {
|
||||
const baseUrl =
|
||||
step.content.type === VideoBubbleContentType.VIMEO
|
||||
block.content.type === VideoBubbleContentType.VIMEO
|
||||
? 'https://player.vimeo.com/video'
|
||||
: 'https://www.youtube.com/embed'
|
||||
return (
|
||||
<Box w="full" h="120px" pos="relative">
|
||||
<iframe
|
||||
src={`${baseUrl}/${step.content.id}`}
|
||||
src={`${baseUrl}/${block.content.id}`}
|
||||
allowFullScreen
|
||||
style={{
|
||||
width: '100%',
|
@ -1,13 +1,13 @@
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { WebhookStep } from 'models'
|
||||
import { WebhookBlock } from 'models'
|
||||
import { byId } from 'utils'
|
||||
|
||||
type Props = {
|
||||
step: WebhookStep
|
||||
block: WebhookBlock
|
||||
}
|
||||
|
||||
export const WebhookContent = ({ step: { webhookId } }: Props) => {
|
||||
export const WebhookContent = ({ block: { webhookId } }: Props) => {
|
||||
const { webhooks } = useTypebot()
|
||||
const webhook = webhooks.find(byId(webhookId))
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { InputStep } from 'models'
|
||||
import { InputBlock } from 'models'
|
||||
import { chakra, Text } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { byId } from 'utils'
|
||||
|
||||
type Props = {
|
||||
step: InputStep
|
||||
block: InputBlock
|
||||
}
|
||||
|
||||
export const WithVariableContent = ({ step }: Props) => {
|
||||
export const WithVariableContent = ({ block }: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
const variableName = typebot?.variables.find(
|
||||
byId(step.options.variableId)
|
||||
byId(block.options.variableId)
|
||||
)?.name
|
||||
|
||||
return (
|
@ -2,27 +2,27 @@ import { Text } from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import {
|
||||
defaultWebhookAttributes,
|
||||
MakeComStep,
|
||||
PabblyConnectStep,
|
||||
MakeComBlock,
|
||||
PabblyConnectBlock,
|
||||
Webhook,
|
||||
ZapierStep,
|
||||
ZapierBlock,
|
||||
} from 'models'
|
||||
import { useEffect } from 'react'
|
||||
import { byId, isNotDefined } from 'utils'
|
||||
|
||||
type Props = {
|
||||
step: ZapierStep | MakeComStep | PabblyConnectStep
|
||||
block: ZapierBlock | MakeComBlock | PabblyConnectBlock
|
||||
configuredLabel: string
|
||||
}
|
||||
|
||||
export const ProviderWebhookContent = ({ step, configuredLabel }: Props) => {
|
||||
export const ProviderWebhookContent = ({ block, configuredLabel }: Props) => {
|
||||
const { webhooks, typebot, updateWebhook } = useTypebot()
|
||||
const webhook = webhooks.find(byId(step.webhookId))
|
||||
const webhook = webhooks.find(byId(block.webhookId))
|
||||
|
||||
useEffect(() => {
|
||||
if (!typebot) return
|
||||
if (!webhook) {
|
||||
const { webhookId } = step
|
||||
const { webhookId } = block
|
||||
const newWebhook = {
|
||||
id: webhookId,
|
||||
...defaultWebhookAttributes,
|
@ -0,0 +1 @@
|
||||
export { BlockNodeContent } from './BlockNodeContent'
|
@ -1,17 +1,15 @@
|
||||
import { MenuList, MenuItem } from '@chakra-ui/react'
|
||||
import { CopyIcon, TrashIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { BlockIndices } from 'models'
|
||||
|
||||
export const BlockNodeContextMenu = ({
|
||||
blockIndex,
|
||||
}: {
|
||||
blockIndex: number
|
||||
}) => {
|
||||
type Props = { indices: BlockIndices }
|
||||
export const BlockNodeContextMenu = ({ indices }: Props) => {
|
||||
const { deleteBlock, duplicateBlock } = useTypebot()
|
||||
|
||||
const handleDeleteClick = () => deleteBlock(blockIndex)
|
||||
const handleDuplicateClick = () => duplicateBlock(indices)
|
||||
|
||||
const handleDuplicateClick = () => duplicateBlock(blockIndex)
|
||||
const handleDeleteClick = () => deleteBlock(indices)
|
||||
|
||||
return (
|
||||
<MenuList>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -1,37 +1,37 @@
|
||||
import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react'
|
||||
import { DraggableStep, DraggableStepType, Step } from 'models'
|
||||
import { DraggableBlock, DraggableBlockType, Block } from 'models'
|
||||
import {
|
||||
computeNearestPlaceholderIndex,
|
||||
useStepDnd,
|
||||
useBlockDnd,
|
||||
} from 'contexts/GraphDndContext'
|
||||
import { Coordinates, useGraph } from 'contexts/GraphContext'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { StepNode } from './StepNode'
|
||||
import { StepNodeOverlay } from './StepNodeOverlay'
|
||||
import { BlockNode } from './BlockNode'
|
||||
import { BlockNodeOverlay } from './BlockNodeOverlay'
|
||||
|
||||
type Props = {
|
||||
blockId: string
|
||||
steps: Step[]
|
||||
blockIndex: number
|
||||
blockRef: React.MutableRefObject<HTMLDivElement | null>
|
||||
isStartBlock: boolean
|
||||
groupId: string
|
||||
blocks: Block[]
|
||||
groupIndex: number
|
||||
groupRef: React.MutableRefObject<HTMLDivElement | null>
|
||||
isStartGroup: boolean
|
||||
}
|
||||
export const StepNodesList = ({
|
||||
blockId,
|
||||
steps,
|
||||
blockIndex,
|
||||
blockRef,
|
||||
isStartBlock,
|
||||
export const BlockNodesList = ({
|
||||
groupId,
|
||||
blocks,
|
||||
groupIndex,
|
||||
groupRef,
|
||||
isStartGroup,
|
||||
}: Props) => {
|
||||
const {
|
||||
draggedStep,
|
||||
setDraggedStep,
|
||||
draggedStepType,
|
||||
mouseOverBlock,
|
||||
setDraggedStepType,
|
||||
} = useStepDnd()
|
||||
const { typebot, createStep, detachStepFromBlock } = useTypebot()
|
||||
draggedBlock,
|
||||
setDraggedBlock,
|
||||
draggedBlockType,
|
||||
mouseOverGroup,
|
||||
setDraggedBlockType,
|
||||
} = useBlockDnd()
|
||||
const { typebot, createBlock, detachBlockFromGroup } = useTypebot()
|
||||
const { isReadOnly, graphPosition } = useGraph()
|
||||
const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState<
|
||||
number | undefined
|
||||
@ -45,17 +45,18 @@ export const StepNodesList = ({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
const isDraggingOnCurrentBlock =
|
||||
(draggedStep || draggedStepType) && mouseOverBlock?.id === blockId
|
||||
const showSortPlaceholders = !isStartBlock && (draggedStep || draggedStepType)
|
||||
const isDraggingOnCurrentGroup =
|
||||
(draggedBlock || draggedBlockType) && mouseOverGroup?.id === groupId
|
||||
const showSortPlaceholders =
|
||||
!isStartGroup && (draggedBlock || draggedBlockType)
|
||||
|
||||
useEffect(() => {
|
||||
if (mouseOverBlock?.id !== blockId) setExpandedPlaceholderIndex(undefined)
|
||||
if (mouseOverGroup?.id !== groupId) setExpandedPlaceholderIndex(undefined)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mouseOverBlock?.id])
|
||||
}, [mouseOverGroup?.id])
|
||||
|
||||
const handleMouseMoveGlobal = (event: MouseEvent) => {
|
||||
if (!draggedStep || draggedStep.blockId !== blockId) return
|
||||
if (!draggedBlock || draggedBlock.groupId !== groupId) return
|
||||
const { clientX, clientY } = event
|
||||
setPosition({
|
||||
x: clientX - mousePositionInElement.x,
|
||||
@ -63,41 +64,44 @@ export const StepNodesList = ({
|
||||
})
|
||||
}
|
||||
|
||||
const handleMouseMoveOnBlock = (event: MouseEvent) => {
|
||||
if (!isDraggingOnCurrentBlock) return
|
||||
const handleMouseMoveOnGroup = (event: MouseEvent) => {
|
||||
if (!isDraggingOnCurrentGroup) return
|
||||
setExpandedPlaceholderIndex(
|
||||
computeNearestPlaceholderIndex(event.pageY, placeholderRefs)
|
||||
)
|
||||
}
|
||||
|
||||
const handleMouseUpOnBlock = (e: MouseEvent) => {
|
||||
const handleMouseUpOnGroup = (e: MouseEvent) => {
|
||||
setExpandedPlaceholderIndex(undefined)
|
||||
if (!isDraggingOnCurrentBlock) return
|
||||
const stepIndex = computeNearestPlaceholderIndex(e.clientY, placeholderRefs)
|
||||
createStep(
|
||||
blockId,
|
||||
(draggedStep || draggedStepType) as DraggableStep | DraggableStepType,
|
||||
if (!isDraggingOnCurrentGroup) return
|
||||
const blockIndex = computeNearestPlaceholderIndex(
|
||||
e.clientY,
|
||||
placeholderRefs
|
||||
)
|
||||
createBlock(
|
||||
groupId,
|
||||
(draggedBlock || draggedBlockType) as DraggableBlock | DraggableBlockType,
|
||||
{
|
||||
groupIndex,
|
||||
blockIndex,
|
||||
stepIndex,
|
||||
}
|
||||
)
|
||||
setDraggedStep(undefined)
|
||||
setDraggedStepType(undefined)
|
||||
setDraggedBlock(undefined)
|
||||
setDraggedBlockType(undefined)
|
||||
}
|
||||
|
||||
const handleStepMouseDown =
|
||||
(stepIndex: number) =>
|
||||
const handleBlockMouseDown =
|
||||
(blockIndex: number) =>
|
||||
(
|
||||
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
|
||||
step: DraggableStep
|
||||
block: DraggableBlock
|
||||
) => {
|
||||
if (isReadOnly) return
|
||||
placeholderRefs.current.splice(stepIndex + 1, 1)
|
||||
detachStepFromBlock({ blockIndex, stepIndex })
|
||||
placeholderRefs.current.splice(blockIndex + 1, 1)
|
||||
detachBlockFromGroup({ groupIndex, blockIndex })
|
||||
setPosition(absolute)
|
||||
setMousePositionInElement(relative)
|
||||
setDraggedStep(step)
|
||||
setDraggedBlock(block)
|
||||
}
|
||||
|
||||
const handlePushElementRef =
|
||||
@ -106,11 +110,11 @@ export const StepNodesList = ({
|
||||
}
|
||||
|
||||
useEventListener('mousemove', handleMouseMoveGlobal)
|
||||
useEventListener('mousemove', handleMouseMoveOnBlock, blockRef.current)
|
||||
useEventListener('mousemove', handleMouseMoveOnGroup, groupRef.current)
|
||||
useEventListener(
|
||||
'mouseup',
|
||||
handleMouseUpOnBlock,
|
||||
mouseOverBlock?.ref.current,
|
||||
handleMouseUpOnGroup,
|
||||
mouseOverGroup?.ref.current,
|
||||
{
|
||||
capture: true,
|
||||
}
|
||||
@ -119,7 +123,7 @@ export const StepNodesList = ({
|
||||
<Stack
|
||||
spacing={1}
|
||||
transition="none"
|
||||
pointerEvents={isReadOnly || isStartBlock ? 'none' : 'auto'}
|
||||
pointerEvents={isReadOnly || isStartGroup ? 'none' : 'auto'}
|
||||
>
|
||||
<Flex
|
||||
ref={handlePushElementRef(0)}
|
||||
@ -134,14 +138,14 @@ export const StepNodesList = ({
|
||||
transition={showSortPlaceholders ? 'height 200ms' : 'none'}
|
||||
/>
|
||||
{typebot &&
|
||||
steps.map((step, idx) => (
|
||||
<Stack key={step.id} spacing={1}>
|
||||
<StepNode
|
||||
key={step.id}
|
||||
step={step}
|
||||
indices={{ blockIndex, stepIndex: idx }}
|
||||
isConnectable={steps.length - 1 === idx}
|
||||
onMouseDown={handleStepMouseDown(idx)}
|
||||
blocks.map((block, idx) => (
|
||||
<Stack key={block.id} spacing={1}>
|
||||
<BlockNode
|
||||
key={block.id}
|
||||
block={block}
|
||||
indices={{ groupIndex, blockIndex: idx }}
|
||||
isConnectable={blocks.length - 1 === idx}
|
||||
onMouseDown={handleBlockMouseDown(idx)}
|
||||
/>
|
||||
<Flex
|
||||
ref={handlePushElementRef(idx + 1)}
|
||||
@ -157,11 +161,11 @@ export const StepNodesList = ({
|
||||
/>
|
||||
</Stack>
|
||||
))}
|
||||
{draggedStep && draggedStep.blockId === blockId && (
|
||||
{draggedBlock && draggedBlock.groupId === groupId && (
|
||||
<Portal>
|
||||
<StepNodeOverlay
|
||||
step={draggedStep}
|
||||
indices={{ blockIndex, stepIndex: 0 }}
|
||||
<BlockNodeOverlay
|
||||
block={draggedBlock}
|
||||
indices={{ groupIndex, blockIndex: 0 }}
|
||||
pos="fixed"
|
||||
top="0"
|
||||
left="0"
|
@ -6,18 +6,18 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import { ImageUploadContent } from 'components/shared/ImageUploadContent'
|
||||
import {
|
||||
BubbleStep,
|
||||
BubbleStepContent,
|
||||
BubbleStepType,
|
||||
TextBubbleStep,
|
||||
BubbleBlock,
|
||||
BubbleBlockContent,
|
||||
BubbleBlockType,
|
||||
TextBubbleBlock,
|
||||
} from 'models'
|
||||
import { useRef } from 'react'
|
||||
import { EmbedUploadContent } from './EmbedUploadContent'
|
||||
import { VideoUploadContent } from './VideoUploadContent'
|
||||
|
||||
type Props = {
|
||||
step: Exclude<BubbleStep, TextBubbleStep>
|
||||
onContentChange: (content: BubbleStepContent) => void
|
||||
block: Exclude<BubbleBlock, TextBubbleBlock>
|
||||
onContentChange: (content: BubbleBlockContent) => void
|
||||
}
|
||||
|
||||
export const MediaBubblePopoverContent = (props: Props) => {
|
||||
@ -28,7 +28,7 @@ export const MediaBubblePopoverContent = (props: Props) => {
|
||||
<Portal>
|
||||
<PopoverContent
|
||||
onMouseDown={handleMouseDown}
|
||||
w={props.step.type === BubbleStepType.IMAGE ? '500px' : '400px'}
|
||||
w={props.block.type === BubbleBlockType.IMAGE ? '500px' : '400px'}
|
||||
>
|
||||
<PopoverArrow />
|
||||
<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 })
|
||||
|
||||
switch (step.type) {
|
||||
case BubbleStepType.IMAGE: {
|
||||
switch (block.type) {
|
||||
case BubbleBlockType.IMAGE: {
|
||||
return (
|
||||
<ImageUploadContent
|
||||
url={step.content?.url}
|
||||
url={block.content?.url}
|
||||
onSubmit={handleImageUrlChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case BubbleStepType.VIDEO: {
|
||||
case BubbleBlockType.VIDEO: {
|
||||
return (
|
||||
<VideoUploadContent content={step.content} onSubmit={onContentChange} />
|
||||
<VideoUploadContent
|
||||
content={block.content}
|
||||
onSubmit={onContentChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case BubbleStepType.EMBED: {
|
||||
case BubbleBlockType.EMBED: {
|
||||
return (
|
||||
<EmbedUploadContent content={step.content} onSubmit={onContentChange} />
|
||||
<EmbedUploadContent
|
||||
content={block.content}
|
||||
onSubmit={onContentChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
@ -9,13 +9,13 @@ import {
|
||||
import { ExpandIcon } from 'assets/icons'
|
||||
import {
|
||||
ConditionItem,
|
||||
ConditionStep,
|
||||
InputStepType,
|
||||
IntegrationStepType,
|
||||
LogicStepType,
|
||||
Step,
|
||||
StepOptions,
|
||||
StepWithOptions,
|
||||
ConditionBlock,
|
||||
InputBlockType,
|
||||
IntegrationBlockType,
|
||||
LogicBlockType,
|
||||
Block,
|
||||
BlockOptions,
|
||||
BlockWithOptions,
|
||||
Webhook,
|
||||
} from 'models'
|
||||
import { useRef } from 'react'
|
||||
@ -42,10 +42,10 @@ import { WebhookSettings } from './bodies/WebhookSettings'
|
||||
import { ZapierSettings } from './bodies/ZapierSettings'
|
||||
|
||||
type Props = {
|
||||
step: StepWithOptions | ConditionStep
|
||||
block: BlockWithOptions | ConditionBlock
|
||||
webhook?: Webhook
|
||||
onExpandClick: () => void
|
||||
onStepChange: (updates: Partial<Step>) => void
|
||||
onBlockChange: (updates: Partial<Block>) => void
|
||||
}
|
||||
|
||||
export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
|
||||
@ -68,7 +68,7 @@ export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
|
||||
ref={ref}
|
||||
shadow="lg"
|
||||
>
|
||||
<StepSettings {...props} />
|
||||
<BlockSettings {...props} />
|
||||
</PopoverBody>
|
||||
<IconButton
|
||||
pos="absolute"
|
||||
@ -84,156 +84,156 @@ export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const StepSettings = ({
|
||||
step,
|
||||
onStepChange,
|
||||
export const BlockSettings = ({
|
||||
block,
|
||||
onBlockChange,
|
||||
}: {
|
||||
step: StepWithOptions | ConditionStep
|
||||
block: BlockWithOptions | ConditionBlock
|
||||
webhook?: Webhook
|
||||
onStepChange: (step: Partial<Step>) => void
|
||||
onBlockChange: (block: Partial<Block>) => void
|
||||
}): JSX.Element => {
|
||||
const handleOptionsChange = (options: StepOptions) => {
|
||||
onStepChange({ options } as Partial<Step>)
|
||||
const handleOptionsChange = (options: BlockOptions) => {
|
||||
onBlockChange({ options } as Partial<Block>)
|
||||
}
|
||||
const handleItemChange = (updates: Partial<ConditionItem>) => {
|
||||
onStepChange({
|
||||
items: [{ ...(step as ConditionStep).items[0], ...updates }],
|
||||
} as Partial<Step>)
|
||||
onBlockChange({
|
||||
items: [{ ...(block as ConditionBlock).items[0], ...updates }],
|
||||
} as Partial<Block>)
|
||||
}
|
||||
switch (step.type) {
|
||||
case InputStepType.TEXT: {
|
||||
switch (block.type) {
|
||||
case InputBlockType.TEXT: {
|
||||
return (
|
||||
<TextInputSettingsBody
|
||||
options={step.options}
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case InputStepType.NUMBER: {
|
||||
case InputBlockType.NUMBER: {
|
||||
return (
|
||||
<NumberInputSettingsBody
|
||||
options={step.options}
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case InputStepType.EMAIL: {
|
||||
case InputBlockType.EMAIL: {
|
||||
return (
|
||||
<EmailInputSettingsBody
|
||||
options={step.options}
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case InputStepType.URL: {
|
||||
case InputBlockType.URL: {
|
||||
return (
|
||||
<UrlInputSettingsBody
|
||||
options={step.options}
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case InputStepType.DATE: {
|
||||
case InputBlockType.DATE: {
|
||||
return (
|
||||
<DateInputSettingsBody
|
||||
options={step.options}
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case InputStepType.PHONE: {
|
||||
case InputBlockType.PHONE: {
|
||||
return (
|
||||
<PhoneNumberSettingsBody
|
||||
options={step.options}
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case InputStepType.CHOICE: {
|
||||
case InputBlockType.CHOICE: {
|
||||
return (
|
||||
<ChoiceInputSettingsBody
|
||||
options={step.options}
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case InputStepType.PAYMENT: {
|
||||
case InputBlockType.PAYMENT: {
|
||||
return (
|
||||
<PaymentSettings
|
||||
options={step.options}
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case InputStepType.RATING: {
|
||||
case InputBlockType.RATING: {
|
||||
return (
|
||||
<RatingInputSettings
|
||||
options={step.options}
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case LogicStepType.SET_VARIABLE: {
|
||||
case LogicBlockType.SET_VARIABLE: {
|
||||
return (
|
||||
<SetVariableSettings
|
||||
options={step.options}
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case LogicStepType.CONDITION: {
|
||||
case LogicBlockType.CONDITION: {
|
||||
return (
|
||||
<ConditionSettingsBody step={step} onItemChange={handleItemChange} />
|
||||
<ConditionSettingsBody block={block} onItemChange={handleItemChange} />
|
||||
)
|
||||
}
|
||||
case LogicStepType.REDIRECT: {
|
||||
case LogicBlockType.REDIRECT: {
|
||||
return (
|
||||
<RedirectSettings
|
||||
options={step.options}
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case LogicStepType.CODE: {
|
||||
case LogicBlockType.CODE: {
|
||||
return (
|
||||
<CodeSettings
|
||||
options={step.options}
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case LogicStepType.TYPEBOT_LINK: {
|
||||
case LogicBlockType.TYPEBOT_LINK: {
|
||||
return (
|
||||
<TypebotLinkSettingsForm
|
||||
options={step.options}
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case IntegrationStepType.GOOGLE_SHEETS: {
|
||||
case IntegrationBlockType.GOOGLE_SHEETS: {
|
||||
return (
|
||||
<GoogleSheetsSettingsBody
|
||||
options={step.options}
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
stepId={step.id}
|
||||
blockId={block.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case IntegrationStepType.GOOGLE_ANALYTICS: {
|
||||
case IntegrationBlockType.GOOGLE_ANALYTICS: {
|
||||
return (
|
||||
<GoogleAnalyticsSettings
|
||||
options={step.options}
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case IntegrationStepType.ZAPIER: {
|
||||
return <ZapierSettings step={step} />
|
||||
case IntegrationBlockType.ZAPIER: {
|
||||
return <ZapierSettings block={block} />
|
||||
}
|
||||
case IntegrationStepType.MAKE_COM: {
|
||||
case IntegrationBlockType.MAKE_COM: {
|
||||
return (
|
||||
<WebhookSettings
|
||||
step={step}
|
||||
block={block}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
provider={{
|
||||
name: 'Make.com',
|
||||
@ -242,10 +242,10 @@ export const StepSettings = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
case IntegrationStepType.PABBLY_CONNECT: {
|
||||
case IntegrationBlockType.PABBLY_CONNECT: {
|
||||
return (
|
||||
<WebhookSettings
|
||||
step={step}
|
||||
block={block}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
provider={{
|
||||
name: 'Pabbly Connect',
|
||||
@ -254,15 +254,15 @@ export const StepSettings = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
case IntegrationStepType.WEBHOOK: {
|
||||
case IntegrationBlockType.WEBHOOK: {
|
||||
return (
|
||||
<WebhookSettings step={step} onOptionsChange={handleOptionsChange} />
|
||||
<WebhookSettings block={block} onOptionsChange={handleOptionsChange} />
|
||||
)
|
||||
}
|
||||
case IntegrationStepType.EMAIL: {
|
||||
case IntegrationBlockType.EMAIL: {
|
||||
return (
|
||||
<SendEmailSettings
|
||||
options={step.options}
|
||||
options={block.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
@ -4,22 +4,22 @@ import { TableList } from 'components/shared/TableList'
|
||||
import {
|
||||
Comparison,
|
||||
ConditionItem,
|
||||
ConditionStep,
|
||||
ConditionBlock,
|
||||
LogicalOperator,
|
||||
} from 'models'
|
||||
import React from 'react'
|
||||
import { ComparisonItem } from './ComparisonsItem'
|
||||
|
||||
type ConditionSettingsBodyProps = {
|
||||
step: ConditionStep
|
||||
block: ConditionBlock
|
||||
onItemChange: (updates: Partial<ConditionItem>) => void
|
||||
}
|
||||
|
||||
export const ConditionSettingsBody = ({
|
||||
step,
|
||||
block,
|
||||
onItemChange,
|
||||
}: ConditionSettingsBodyProps) => {
|
||||
const itemContent = step.items[0].content
|
||||
const itemContent = block.items[0].content
|
||||
|
||||
const handleComparisonsChange = (comparisons: Comparison[]) =>
|
||||
onItemChange({ content: { ...itemContent, comparisons } })
|
@ -21,11 +21,15 @@ import { getGoogleSheetsConsentScreenUrl } from 'services/integrations'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
stepId: string
|
||||
blockId: string
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const GoogleSheetConnectModal = ({ stepId, isOpen, onClose }: Props) => {
|
||||
export const GoogleSheetConnectModal = ({
|
||||
blockId,
|
||||
isOpen,
|
||||
onClose,
|
||||
}: Props) => {
|
||||
const { workspace } = useWorkspace()
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="lg">
|
||||
@ -56,7 +60,7 @@ export const GoogleSheetConnectModal = ({ stepId, isOpen, onClose }: Props) => {
|
||||
variant="outline"
|
||||
href={getGoogleSheetsConsentScreenUrl(
|
||||
window.location.href,
|
||||
stepId,
|
||||
blockId,
|
||||
workspace?.id
|
||||
)}
|
||||
mx="auto"
|
@ -25,13 +25,13 @@ import { GoogleSheetConnectModal } from './GoogleSheetsConnectModal'
|
||||
type Props = {
|
||||
options: GoogleSheetsOptions
|
||||
onOptionsChange: (options: GoogleSheetsOptions) => void
|
||||
stepId: string
|
||||
blockId: string
|
||||
}
|
||||
|
||||
export const GoogleSheetsSettingsBody = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
stepId,
|
||||
blockId,
|
||||
}: Props) => {
|
||||
const { save } = useTypebot()
|
||||
const { sheets, isLoading } = useSheets({
|
||||
@ -93,7 +93,7 @@ export const GoogleSheetsSettingsBody = ({
|
||||
onCreateNewClick={handleCreateNewClick}
|
||||
/>
|
||||
<GoogleSheetConnectModal
|
||||
stepId={stepId}
|
||||
blockId={blockId}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
/>
|
@ -23,8 +23,8 @@ export const NumberInputSettingsBody = ({
|
||||
onOptionsChange(removeUndefinedFields({ ...options, min }))
|
||||
const handleMaxChange = (max?: number) =>
|
||||
onOptionsChange(removeUndefinedFields({ ...options, max }))
|
||||
const handleStepChange = (step?: number) =>
|
||||
onOptionsChange(removeUndefinedFields({ ...options, step }))
|
||||
const handleBlockChange = (block?: number) =>
|
||||
onOptionsChange(removeUndefinedFields({ ...options, block }))
|
||||
const handleVariableChange = (variable?: Variable) => {
|
||||
onOptionsChange({ ...options, variableId: variable?.id })
|
||||
}
|
||||
@ -78,7 +78,7 @@ export const NumberInputSettingsBody = ({
|
||||
<SmartNumberInput
|
||||
id="step"
|
||||
value={options.step}
|
||||
onValueChange={handleStepChange}
|
||||
onValueChange={handleBlockChange}
|
||||
/>
|
||||
</HStack>
|
||||
<Stack>
|
@ -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'}
|
||||
/>
|
||||
)
|
||||
}
|
@ -2,7 +2,7 @@ import { Stack } from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { TypebotLinkOptions } from 'models'
|
||||
import { byId } from 'utils'
|
||||
import { BlocksDropdown } from './BlocksDropdown'
|
||||
import { GroupsDropdown } from './GroupsDropdown'
|
||||
import { TypebotsDropdown } from './TypebotsDropdown'
|
||||
|
||||
type Props = {
|
||||
@ -18,8 +18,8 @@ export const TypebotLinkSettingsForm = ({
|
||||
|
||||
const handleTypebotIdChange = (typebotId: string | 'current') =>
|
||||
onOptionsChange({ ...options, typebotId })
|
||||
const handleBlockIdChange = (blockId: string) =>
|
||||
onOptionsChange({ ...options, blockId })
|
||||
const handleGroupIdChange = (groupId: string) =>
|
||||
onOptionsChange({ ...options, groupId })
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
@ -30,15 +30,15 @@ export const TypebotLinkSettingsForm = ({
|
||||
currentWorkspaceId={typebot.workspaceId as string}
|
||||
/>
|
||||
)}
|
||||
<BlocksDropdown
|
||||
blocks={
|
||||
<GroupsDropdown
|
||||
groups={
|
||||
typebot &&
|
||||
(options.typebotId === typebot.id || options.typebotId === 'current')
|
||||
? typebot.blocks
|
||||
: linkedTypebots?.find(byId(options.typebotId))?.blocks ?? []
|
||||
? typebot.groups
|
||||
: linkedTypebots?.find(byId(options.typebotId))?.groups ?? []
|
||||
}
|
||||
blockId={options.blockId}
|
||||
onBlockIdSelected={handleBlockIdChange}
|
||||
groupId={options.groupId}
|
||||
onGroupIdSelected={handleGroupIdChange}
|
||||
isLoading={
|
||||
linkedTypebots === undefined &&
|
||||
typebot &&
|
@ -22,11 +22,11 @@ import {
|
||||
WebhookOptions,
|
||||
VariableForTest,
|
||||
ResponseVariableMapping,
|
||||
WebhookStep,
|
||||
WebhookBlock,
|
||||
defaultWebhookAttributes,
|
||||
Webhook,
|
||||
MakeComStep,
|
||||
PabblyConnectStep,
|
||||
MakeComBlock,
|
||||
PabblyConnectBlock,
|
||||
} from 'models'
|
||||
import { DropdownList } from 'components/shared/DropdownList'
|
||||
import { TableList, TableListItemProps } from 'components/shared/TableList'
|
||||
@ -49,13 +49,13 @@ type Provider = {
|
||||
url: string
|
||||
}
|
||||
type Props = {
|
||||
step: WebhookStep | MakeComStep | PabblyConnectStep
|
||||
block: WebhookBlock | MakeComBlock | PabblyConnectBlock
|
||||
onOptionsChange: (options: WebhookOptions) => void
|
||||
provider?: Provider
|
||||
}
|
||||
|
||||
export const WebhookSettings = ({
|
||||
step: { options, blockId, id: stepId, webhookId },
|
||||
block: { options, id: blockId, webhookId },
|
||||
onOptionsChange,
|
||||
provider,
|
||||
}: Props) => {
|
||||
@ -135,7 +135,7 @@ export const WebhookSettings = ({
|
||||
options.variablesForTest,
|
||||
typebot.variables
|
||||
),
|
||||
{ blockId, stepId }
|
||||
{ blockId }
|
||||
)
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
@ -9,17 +9,17 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import { ExternalLinkIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { ZapierStep } from 'models'
|
||||
import { ZapierBlock } from 'models'
|
||||
import React from 'react'
|
||||
import { byId } from 'utils'
|
||||
|
||||
type Props = {
|
||||
step: ZapierStep
|
||||
block: ZapierBlock
|
||||
}
|
||||
|
||||
export const ZapierSettings = ({ step }: Props) => {
|
||||
export const ZapierSettings = ({ block }: Props) => {
|
||||
const { webhooks } = useTypebot()
|
||||
const webhook = webhooks.find(byId(step.webhookId))
|
||||
const webhook = webhooks.find(byId(block.webhookId))
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
@ -33,7 +33,7 @@ export const ZapierSettings = ({ step }: Props) => {
|
||||
<>Your zap is correctly configured 🚀</>
|
||||
) : (
|
||||
<Stack>
|
||||
<Text>Head up to Zapier to configure this step:</Text>
|
||||
<Text>Head up to Zapier to configure this block:</Text>
|
||||
<Button
|
||||
as={Link}
|
||||
href="https://zapier.com/apps/typebot/integrations"
|
@ -40,7 +40,7 @@ export const TextBubbleEditor = ({ initialValue, onClose }: Props) => {
|
||||
|
||||
const textEditorRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const closeEditor = () => onClose(convertValueToStepContent(value))
|
||||
const closeEditor = () => onClose(convertValueToBlockContent(value))
|
||||
|
||||
useOutsideClick({
|
||||
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
|
||||
const html = serializeHtml(editor, {
|
||||
nodes: value,
|
@ -1 +1 @@
|
||||
export { BlockNode } from './BlockNode'
|
||||
export { BlockNodesList } from './BlockNodesList'
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -1,15 +1,17 @@
|
||||
import { MenuList, MenuItem } from '@chakra-ui/react'
|
||||
import { CopyIcon, TrashIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { StepIndices } from 'models'
|
||||
|
||||
type Props = { indices: StepIndices }
|
||||
export const StepNodeContextMenu = ({ indices }: Props) => {
|
||||
const { deleteStep, duplicateStep } = useTypebot()
|
||||
export const GroupNodeContextMenu = ({
|
||||
groupIndex,
|
||||
}: {
|
||||
groupIndex: number
|
||||
}) => {
|
||||
const { deleteGroup, duplicateGroup } = useTypebot()
|
||||
|
||||
const handleDuplicateClick = () => duplicateStep(indices)
|
||||
const handleDeleteClick = () => deleteGroup(groupIndex)
|
||||
|
||||
const handleDeleteClick = () => deleteStep(indices)
|
||||
const handleDuplicateClick = () => duplicateGroup(groupIndex)
|
||||
|
||||
return (
|
||||
<MenuList>
|
@ -0,0 +1 @@
|
||||
export { GroupNode } from './GroupNode'
|
@ -5,7 +5,7 @@ import { NodePosition, useDragDistance } from 'contexts/GraphDndContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import {
|
||||
ButtonItem,
|
||||
ChoiceInputStep,
|
||||
ChoiceInputBlock,
|
||||
Item,
|
||||
ItemIndices,
|
||||
ItemType,
|
||||
@ -21,7 +21,7 @@ type Props = {
|
||||
indices: ItemIndices
|
||||
isReadOnly: boolean
|
||||
onMouseDown?: (
|
||||
stepNodePosition: { absolute: Coordinates; relative: Coordinates },
|
||||
blockNodePosition: { absolute: Coordinates; relative: Coordinates },
|
||||
item: ButtonItem
|
||||
) => void
|
||||
}
|
||||
@ -33,9 +33,9 @@ export const ItemNode = ({ item, indices, isReadOnly, onMouseDown }: Props) => {
|
||||
const itemRef = useRef<HTMLDivElement | null>(null)
|
||||
const isPreviewing = previewingEdge?.from.itemId === item.id
|
||||
const isConnectable = !(
|
||||
typebot?.blocks[indices.blockIndex].steps[
|
||||
indices.stepIndex
|
||||
] as ChoiceInputStep
|
||||
typebot?.groups[indices.groupIndex].blocks[
|
||||
indices.blockIndex
|
||||
] as ChoiceInputBlock
|
||||
)?.options?.isMultipleChoice
|
||||
const onDrag = (position: NodePosition) => {
|
||||
if (!onMouseDown || item.type !== ItemType.BUTTON) return
|
||||
@ -83,8 +83,8 @@ export const ItemNode = ({ item, indices, isReadOnly, onMouseDown }: Props) => {
|
||||
{typebot && isConnectable && (
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
blockId: typebot.blocks[indices.blockIndex].id,
|
||||
stepId: item.stepId,
|
||||
groupId: typebot.groups[indices.groupIndex].id,
|
||||
blockId: item.blockId,
|
||||
itemId: item.id,
|
||||
}}
|
||||
pos="absolute"
|
||||
|
@ -45,7 +45,7 @@ export const ButtonNodeContent = ({ item, indices, isMouseOver }: Props) => {
|
||||
const handlePlusClick = () => {
|
||||
const itemIndex = indices.itemIndex + 1
|
||||
createItem(
|
||||
{ stepId: item.stepId, type: ItemType.BUTTON },
|
||||
{ blockId: item.blockId, type: ItemType.BUTTON },
|
||||
{ ...indices, itemIndex }
|
||||
)
|
||||
}
|
||||
|
@ -1,38 +1,38 @@
|
||||
import { Flex, Portal, Stack, Text, useEventListener } from '@chakra-ui/react'
|
||||
import {
|
||||
computeNearestPlaceholderIndex,
|
||||
useStepDnd,
|
||||
useBlockDnd,
|
||||
} from 'contexts/GraphDndContext'
|
||||
import { Coordinates, useGraph } from 'contexts/GraphContext'
|
||||
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 { ItemNode } from './ItemNode'
|
||||
import { SourceEndpoint } from '../../Endpoints'
|
||||
import { ItemNodeOverlay } from './ItemNodeOverlay'
|
||||
|
||||
type Props = {
|
||||
step: StepWithItems
|
||||
indices: StepIndices
|
||||
block: BlockWithItems
|
||||
indices: BlockIndices
|
||||
isReadOnly?: boolean
|
||||
}
|
||||
|
||||
export const ItemNodesList = ({
|
||||
step,
|
||||
indices: { blockIndex, stepIndex },
|
||||
block,
|
||||
indices: { groupIndex, blockIndex },
|
||||
isReadOnly = false,
|
||||
}: Props) => {
|
||||
const { typebot, createItem, detachItemFromStep } = useTypebot()
|
||||
const { draggedItem, setDraggedItem, mouseOverBlock } = useStepDnd()
|
||||
const { typebot, createItem, detachItemFromBlock } = useTypebot()
|
||||
const { draggedItem, setDraggedItem, mouseOverGroup } = useBlockDnd()
|
||||
const placeholderRefs = useRef<HTMLDivElement[]>([])
|
||||
const { graphPosition } = useGraph()
|
||||
const blockId = typebot?.blocks[blockIndex].id
|
||||
const isDraggingOnCurrentBlock =
|
||||
(draggedItem && mouseOverBlock?.id === blockId) ?? false
|
||||
const groupId = typebot?.groups[groupIndex].id
|
||||
const isDraggingOnCurrentGroup =
|
||||
(draggedItem && mouseOverGroup?.id === groupId) ?? false
|
||||
const showPlaceholders = draggedItem && !isReadOnly
|
||||
|
||||
const isLastStep =
|
||||
typebot?.blocks[blockIndex].steps[stepIndex + 1] === undefined
|
||||
const isLastBlock =
|
||||
typebot?.groups[groupIndex].blocks[blockIndex + 1] === undefined
|
||||
|
||||
const [position, setPosition] = useState({
|
||||
x: 0,
|
||||
@ -44,7 +44,7 @@ export const ItemNodesList = ({
|
||||
>()
|
||||
|
||||
const handleGlobalMouseMove = (event: MouseEvent) => {
|
||||
if (!draggedItem || draggedItem.stepId !== step.id) return
|
||||
if (!draggedItem || draggedItem.blockId !== block.id) return
|
||||
const { clientX, clientY } = event
|
||||
setPosition({
|
||||
...position,
|
||||
@ -55,44 +55,44 @@ export const ItemNodesList = ({
|
||||
useEventListener('mousemove', handleGlobalMouseMove)
|
||||
|
||||
useEffect(() => {
|
||||
if (mouseOverBlock?.id !== step.blockId)
|
||||
if (mouseOverGroup?.id !== block.groupId)
|
||||
setExpandedPlaceholderIndex(undefined)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mouseOverBlock?.id])
|
||||
}, [mouseOverGroup?.id])
|
||||
|
||||
const handleMouseMoveOnBlock = (event: MouseEvent) => {
|
||||
if (!isDraggingOnCurrentBlock || isReadOnly) return
|
||||
const handleMouseMoveOnGroup = (event: MouseEvent) => {
|
||||
if (!isDraggingOnCurrentGroup || isReadOnly) return
|
||||
const index = computeNearestPlaceholderIndex(event.pageY, placeholderRefs)
|
||||
setExpandedPlaceholderIndex(index)
|
||||
}
|
||||
useEventListener(
|
||||
'mousemove',
|
||||
handleMouseMoveOnBlock,
|
||||
mouseOverBlock?.ref.current
|
||||
handleMouseMoveOnGroup,
|
||||
mouseOverGroup?.ref.current
|
||||
)
|
||||
|
||||
const handleMouseUpOnBlock = (e: MouseEvent) => {
|
||||
const handleMouseUpOnGroup = (e: MouseEvent) => {
|
||||
setExpandedPlaceholderIndex(undefined)
|
||||
if (!isDraggingOnCurrentBlock) return
|
||||
if (!isDraggingOnCurrentGroup) return
|
||||
const itemIndex = computeNearestPlaceholderIndex(e.pageY, placeholderRefs)
|
||||
e.stopPropagation()
|
||||
setDraggedItem(undefined)
|
||||
createItem(draggedItem as ButtonItem, {
|
||||
groupIndex,
|
||||
blockIndex,
|
||||
stepIndex,
|
||||
itemIndex,
|
||||
})
|
||||
}
|
||||
useEventListener(
|
||||
'mouseup',
|
||||
handleMouseUpOnBlock,
|
||||
mouseOverBlock?.ref.current,
|
||||
handleMouseUpOnGroup,
|
||||
mouseOverGroup?.ref.current,
|
||||
{
|
||||
capture: true,
|
||||
}
|
||||
)
|
||||
|
||||
const handleStepMouseDown =
|
||||
const handleBlockMouseDown =
|
||||
(itemIndex: number) =>
|
||||
(
|
||||
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
|
||||
@ -100,7 +100,7 @@ export const ItemNodesList = ({
|
||||
) => {
|
||||
if (!typebot || isReadOnly) return
|
||||
placeholderRefs.current.splice(itemIndex + 1, 1)
|
||||
detachItemFromStep({ blockIndex, stepIndex, itemIndex })
|
||||
detachItemFromBlock({ groupIndex, blockIndex, itemIndex })
|
||||
setPosition(absolute)
|
||||
setRelativeCoordinates(relative)
|
||||
setDraggedItem(item)
|
||||
@ -129,12 +129,12 @@ export const ItemNodesList = ({
|
||||
rounded="lg"
|
||||
transition={showPlaceholders ? 'height 200ms' : 'none'}
|
||||
/>
|
||||
{step.items.map((item, idx) => (
|
||||
{block.items.map((item, idx) => (
|
||||
<Stack key={item.id} spacing={1}>
|
||||
<ItemNode
|
||||
item={item}
|
||||
indices={{ blockIndex, stepIndex, itemIndex: idx }}
|
||||
onMouseDown={handleStepMouseDown(idx)}
|
||||
indices={{ groupIndex, blockIndex, itemIndex: idx }}
|
||||
onMouseDown={handleBlockMouseDown(idx)}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
<Flex
|
||||
@ -151,7 +151,7 @@ export const ItemNodesList = ({
|
||||
/>
|
||||
</Stack>
|
||||
))}
|
||||
{isLastStep && (
|
||||
{isLastBlock && (
|
||||
<Flex
|
||||
px="4"
|
||||
py="2"
|
||||
@ -166,8 +166,8 @@ export const ItemNodesList = ({
|
||||
<Text color={isReadOnly ? 'inherit' : 'gray.500'}>Default</Text>
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
blockId: step.blockId,
|
||||
stepId: step.id,
|
||||
groupId: block.groupId,
|
||||
blockId: block.id,
|
||||
}}
|
||||
pos="absolute"
|
||||
right="-49px"
|
||||
@ -175,7 +175,7 @@ export const ItemNodesList = ({
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{draggedItem && draggedItem.stepId === step.id && (
|
||||
{draggedItem && draggedItem.blockId === block.id && (
|
||||
<Portal>
|
||||
<ItemNodeOverlay
|
||||
item={draggedItem}
|
||||
|
@ -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
Reference in New Issue
Block a user