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