feat(inputs): ✨ Add buttons input
This commit is contained in:
@@ -230,3 +230,10 @@ export const PhoneIcon = (props: IconProps) => (
|
|||||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
|
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const CheckSquareIcon = (props: IconProps) => (
|
||||||
|
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||||
|
<polyline points="9 11 12 14 22 4"></polyline>
|
||||||
|
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ export const Edge = ({ stepId }: Props) => {
|
|||||||
|
|
||||||
const path = useMemo(() => {
|
const path = useMemo(() => {
|
||||||
if (!sourceBlock || !targetBlock) return ``
|
if (!sourceBlock || !targetBlock) return ``
|
||||||
const anchorsPosition = getAnchorsPosition(
|
const anchorsPosition = getAnchorsPosition({
|
||||||
sourceBlock,
|
sourceBlock,
|
||||||
targetBlock,
|
targetBlock,
|
||||||
sourceBlock.stepIds.indexOf(stepId),
|
sourceStepIndex: sourceBlock.stepIds.indexOf(stepId),
|
||||||
targetStepIndex
|
sourceChoiceItemIndex: targetStepIndex,
|
||||||
)
|
})
|
||||||
return computeFlowChartConnectorPath(anchorsPosition)
|
return computeFlowChartConnectorPath(anchorsPosition)
|
||||||
}, [sourceBlock, stepId, targetBlock, targetStepIndex])
|
}, [sourceBlock, stepId, targetBlock, targetStepIndex])
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { IconProps } from '@chakra-ui/react'
|
||||||
import {
|
import {
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
ChatIcon,
|
ChatIcon,
|
||||||
|
CheckSquareIcon,
|
||||||
EmailIcon,
|
EmailIcon,
|
||||||
FlagIcon,
|
FlagIcon,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
@@ -11,33 +13,36 @@ import {
|
|||||||
import { BubbleStepType, InputStepType, StepType } from 'models'
|
import { BubbleStepType, InputStepType, StepType } from 'models'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
type StepIconProps = { type: StepType }
|
type StepIconProps = { type: StepType } & IconProps
|
||||||
|
|
||||||
export const StepIcon = ({ type }: StepIconProps) => {
|
export const StepIcon = ({ type, ...props }: StepIconProps) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case BubbleStepType.TEXT: {
|
case BubbleStepType.TEXT: {
|
||||||
return <ChatIcon />
|
return <ChatIcon {...props} />
|
||||||
}
|
}
|
||||||
case InputStepType.TEXT: {
|
case InputStepType.TEXT: {
|
||||||
return <TextIcon />
|
return <TextIcon {...props} />
|
||||||
}
|
}
|
||||||
case InputStepType.NUMBER: {
|
case InputStepType.NUMBER: {
|
||||||
return <NumberIcon />
|
return <NumberIcon {...props} />
|
||||||
}
|
}
|
||||||
case InputStepType.EMAIL: {
|
case InputStepType.EMAIL: {
|
||||||
return <EmailIcon />
|
return <EmailIcon {...props} />
|
||||||
}
|
}
|
||||||
case InputStepType.URL: {
|
case InputStepType.URL: {
|
||||||
return <GlobeIcon />
|
return <GlobeIcon {...props} />
|
||||||
}
|
}
|
||||||
case InputStepType.DATE: {
|
case InputStepType.DATE: {
|
||||||
return <CalendarIcon />
|
return <CalendarIcon {...props} />
|
||||||
}
|
}
|
||||||
case InputStepType.PHONE: {
|
case InputStepType.PHONE: {
|
||||||
return <PhoneIcon />
|
return <PhoneIcon {...props} />
|
||||||
|
}
|
||||||
|
case InputStepType.CHOICE: {
|
||||||
|
return <CheckSquareIcon {...props} />
|
||||||
}
|
}
|
||||||
case 'start': {
|
case 'start': {
|
||||||
return <FlagIcon />
|
return <FlagIcon {...props} />
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return <></>
|
return <></>
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ export const StepTypeLabel = ({ type }: Props) => {
|
|||||||
case InputStepType.PHONE: {
|
case InputStepType.PHONE: {
|
||||||
return <Text>Phone</Text>
|
return <Text>Phone</Text>
|
||||||
}
|
}
|
||||||
|
case InputStepType.CHOICE: {
|
||||||
|
return <Text>Button</Text>
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,11 @@ type Props = {
|
|||||||
|
|
||||||
export const BlockNode = ({ block }: Props) => {
|
export const BlockNode = ({ block }: Props) => {
|
||||||
const { connectingIds, setConnectingIds, previewingIds } = useGraph()
|
const { connectingIds, setConnectingIds, previewingIds } = useGraph()
|
||||||
const { typebot, updateBlock, createStep } = useTypebot()
|
const { typebot, updateBlock } = useTypebot()
|
||||||
const { draggedStep, draggedStepType, setDraggedStepType, setDraggedStep } =
|
const { setMouseOverBlockId } = useDnd()
|
||||||
useDnd()
|
const { draggedStep, draggedStepType } = useDnd()
|
||||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||||
const [titleValue, setTitleValue] = useState(block.title)
|
const [titleValue, setTitleValue] = useState(block.title)
|
||||||
const [showSortPlaceholders, setShowSortPlaceholders] = useState(false)
|
|
||||||
const [isConnecting, setIsConnecting] = useState(false)
|
const [isConnecting, setIsConnecting] = useState(false)
|
||||||
const isPreviewing = useMemo(
|
const isPreviewing = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -66,28 +65,16 @@ export const BlockNode = ({ block }: Props) => {
|
|||||||
useEventListener('mousemove', handleMouseMove)
|
useEventListener('mousemove', handleMouseMove)
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
if (draggedStepType || draggedStep) setShowSortPlaceholders(true)
|
if (draggedStepType || draggedStep) setMouseOverBlockId(block.id)
|
||||||
if (connectingIds)
|
if (connectingIds)
|
||||||
setConnectingIds({ ...connectingIds, target: { blockId: block.id } })
|
setConnectingIds({ ...connectingIds, target: { blockId: block.id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
setShowSortPlaceholders(false)
|
setMouseOverBlockId(undefined)
|
||||||
if (connectingIds) setConnectingIds({ ...connectingIds, target: undefined })
|
if (connectingIds) setConnectingIds({ ...connectingIds, target: undefined })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleStepDrop = (index: number) => {
|
|
||||||
setShowSortPlaceholders(false)
|
|
||||||
if (draggedStepType) {
|
|
||||||
createStep(block.id, draggedStepType, index)
|
|
||||||
setDraggedStepType(undefined)
|
|
||||||
}
|
|
||||||
if (draggedStep) {
|
|
||||||
createStep(block.id, draggedStep, index)
|
|
||||||
setDraggedStep(undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu<HTMLDivElement>
|
<ContextMenu<HTMLDivElement>
|
||||||
renderMenu={() => <BlockNodeContextMenu blockId={block.id} />}
|
renderMenu={() => <BlockNodeContextMenu blockId={block.id} />}
|
||||||
@@ -125,8 +112,6 @@ export const BlockNode = ({ block }: Props) => {
|
|||||||
<StepsList
|
<StepsList
|
||||||
blockId={block.id}
|
blockId={block.id}
|
||||||
steps={filterTable(block.stepIds, typebot?.steps)}
|
steps={filterTable(block.stepIds, typebot?.steps)}
|
||||||
showSortPlaceholders={showSortPlaceholders}
|
|
||||||
onMouseUp={handleStepDrop}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import {
|
||||||
|
EditablePreview,
|
||||||
|
EditableInput,
|
||||||
|
Editable,
|
||||||
|
useEventListener,
|
||||||
|
Flex,
|
||||||
|
Fade,
|
||||||
|
IconButton,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { PlusIcon } from 'assets/icons'
|
||||||
|
import { ContextMenu } from 'components/shared/ContextMenu'
|
||||||
|
import { Coordinates } from 'contexts/GraphContext'
|
||||||
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
|
import { ChoiceInputStep, ChoiceItem } from 'models'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { isDefined, isSingleChoiceInput } from 'utils'
|
||||||
|
import { SourceEndpoint } from '../SourceEndpoint'
|
||||||
|
import { ChoiceItemNodeContextMenu } from './ChoiceItemNodeContextMenu'
|
||||||
|
|
||||||
|
type ChoiceItemNodeProps = {
|
||||||
|
item: ChoiceItem
|
||||||
|
onMouseMoveBottomOfElement?: () => void
|
||||||
|
onMouseMoveTopOfElement?: () => void
|
||||||
|
onMouseDown?: (
|
||||||
|
stepNodePosition: { absolute: Coordinates; relative: Coordinates },
|
||||||
|
item: ChoiceItem
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChoiceItemNode = ({
|
||||||
|
item,
|
||||||
|
onMouseDown,
|
||||||
|
onMouseMoveBottomOfElement,
|
||||||
|
onMouseMoveTopOfElement,
|
||||||
|
}: ChoiceItemNodeProps) => {
|
||||||
|
const { deleteChoiceItem, updateChoiceItem, typebot, createChoiceItem } =
|
||||||
|
useTypebot()
|
||||||
|
const [mouseDownEvent, setMouseDownEvent] =
|
||||||
|
useState<{ absolute: Coordinates; relative: Coordinates }>()
|
||||||
|
const [isMouseOver, setIsMouseOver] = useState(false)
|
||||||
|
|
||||||
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
|
if (!onMouseDown) return
|
||||||
|
e.stopPropagation()
|
||||||
|
const element = e.currentTarget as HTMLDivElement
|
||||||
|
const rect = element.getBoundingClientRect()
|
||||||
|
const relativeX = e.clientX - rect.left
|
||||||
|
const relativeY = e.clientY - rect.top
|
||||||
|
setMouseDownEvent({
|
||||||
|
absolute: { x: e.clientX + relativeX, y: e.clientY + relativeY },
|
||||||
|
relative: { x: relativeX, y: relativeY },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGlobalMouseUp = () => {
|
||||||
|
setMouseDownEvent(undefined)
|
||||||
|
}
|
||||||
|
useEventListener('mouseup', handleGlobalMouseUp)
|
||||||
|
|
||||||
|
const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (!onMouseMoveBottomOfElement || !onMouseMoveTopOfElement) return
|
||||||
|
const isMovingAndIsMouseDown =
|
||||||
|
mouseDownEvent &&
|
||||||
|
onMouseDown &&
|
||||||
|
(event.movementX > 0 || event.movementY > 0)
|
||||||
|
if (isMovingAndIsMouseDown) {
|
||||||
|
onMouseDown(mouseDownEvent, item)
|
||||||
|
deleteChoiceItem(item.id)
|
||||||
|
setMouseDownEvent(undefined)
|
||||||
|
}
|
||||||
|
const element = event.currentTarget as HTMLDivElement
|
||||||
|
const rect = element.getBoundingClientRect()
|
||||||
|
const y = event.clientY - rect.top
|
||||||
|
if (y > rect.height / 2) onMouseMoveBottomOfElement()
|
||||||
|
else onMouseMoveTopOfElement()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInputSubmit = (content: string) =>
|
||||||
|
updateChoiceItem(item.id, { content: content === '' ? undefined : content })
|
||||||
|
|
||||||
|
const handlePlusClick = () => {
|
||||||
|
const nextIndex =
|
||||||
|
(
|
||||||
|
typebot?.steps.byId[item.stepId] as ChoiceInputStep
|
||||||
|
).options.itemIds.indexOf(item.id) + 1
|
||||||
|
createChoiceItem({ stepId: item.stepId }, nextIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseEnter = () => setIsMouseOver(true)
|
||||||
|
const handleMouseLeave = () => setIsMouseOver(false)
|
||||||
|
return (
|
||||||
|
<ContextMenu<HTMLDivElement>
|
||||||
|
renderMenu={() => <ChoiceItemNodeContextMenu itemId={item.id} />}
|
||||||
|
>
|
||||||
|
{(ref, isOpened) => (
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
pos="relative"
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<Editable
|
||||||
|
ref={ref}
|
||||||
|
px="4"
|
||||||
|
py="2"
|
||||||
|
rounded="md"
|
||||||
|
bgColor="green.200"
|
||||||
|
borderWidth="2px"
|
||||||
|
borderColor={isOpened ? 'blue.400' : 'gray.400'}
|
||||||
|
defaultValue={item.content ?? 'Click to edit'}
|
||||||
|
flex="1"
|
||||||
|
startWithEditView={!isDefined(item.content)}
|
||||||
|
onSubmit={handleInputSubmit}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
>
|
||||||
|
<EditablePreview />
|
||||||
|
<EditableInput />
|
||||||
|
</Editable>
|
||||||
|
{typebot && isSingleChoiceInput(typebot.steps.byId[item.stepId]) && (
|
||||||
|
<SourceEndpoint
|
||||||
|
source={{
|
||||||
|
blockId: typebot.steps.byId[item.stepId].blockId,
|
||||||
|
stepId: item.stepId,
|
||||||
|
choiceItemId: item.id,
|
||||||
|
}}
|
||||||
|
pos="absolute"
|
||||||
|
right="15px"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Fade
|
||||||
|
in={isMouseOver}
|
||||||
|
style={{ position: 'absolute', bottom: '-15px', zIndex: 3 }}
|
||||||
|
unmountOnExit
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Add item"
|
||||||
|
icon={<PlusIcon />}
|
||||||
|
size="xs"
|
||||||
|
onClick={handlePlusClick}
|
||||||
|
/>
|
||||||
|
</Fade>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</ContextMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { MenuList, MenuItem } from '@chakra-ui/react'
|
||||||
|
import { TrashIcon } from 'assets/icons'
|
||||||
|
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||||
|
|
||||||
|
export const ChoiceItemNodeContextMenu = ({ itemId }: { itemId: string }) => {
|
||||||
|
const { deleteChoiceItem } = useTypebot()
|
||||||
|
|
||||||
|
const handleDeleteClick = () => deleteChoiceItem(itemId)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem icon={<TrashIcon />} onClick={handleDeleteClick}>
|
||||||
|
Delete
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Flex, FlexProps } from '@chakra-ui/react'
|
||||||
|
import { ChoiceItem } from 'models'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
type ChoiceItemNodeOverlayProps = {
|
||||||
|
item: ChoiceItem
|
||||||
|
} & FlexProps
|
||||||
|
|
||||||
|
export const ChoiceItemNodeOverlay = ({
|
||||||
|
item,
|
||||||
|
...props
|
||||||
|
}: ChoiceItemNodeOverlayProps) => {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
px="4"
|
||||||
|
py="2"
|
||||||
|
rounded="md"
|
||||||
|
bgColor="green.200"
|
||||||
|
borderWidth="2px"
|
||||||
|
borderColor={'gray.400'}
|
||||||
|
w="212px"
|
||||||
|
pointerEvents="none"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{item.content ?? 'Click to edit'}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { Flex, Portal, Stack, Text, useEventListener } from '@chakra-ui/react'
|
||||||
|
import { useDnd } from 'contexts/DndContext'
|
||||||
|
import { Coordinates } from 'contexts/GraphContext'
|
||||||
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
|
import { ChoiceInputStep, ChoiceItem } from 'models'
|
||||||
|
import React, { useMemo, useState } from 'react'
|
||||||
|
import { SourceEndpoint } from '../SourceEndpoint'
|
||||||
|
import { ChoiceItemNode } from './ChoiceItemNode'
|
||||||
|
import { ChoiceItemNodeOverlay } from './ChoiceItemNodeOverlay'
|
||||||
|
|
||||||
|
type ChoiceItemsListProps = {
|
||||||
|
step: ChoiceInputStep
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChoiceItemsList = ({ step }: ChoiceItemsListProps) => {
|
||||||
|
const { typebot, createChoiceItem } = useTypebot()
|
||||||
|
const {
|
||||||
|
draggedChoiceItem,
|
||||||
|
mouseOverBlockId,
|
||||||
|
setDraggedChoiceItem,
|
||||||
|
setMouseOverBlockId,
|
||||||
|
} = useDnd()
|
||||||
|
const showSortPlaceholders = useMemo(
|
||||||
|
() => mouseOverBlockId === step.blockId && draggedChoiceItem,
|
||||||
|
[draggedChoiceItem, mouseOverBlockId, step.blockId]
|
||||||
|
)
|
||||||
|
const [position, setPosition] = useState({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
})
|
||||||
|
const [relativeCoordinates, setRelativeCoordinates] = useState({ x: 0, y: 0 })
|
||||||
|
const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState<
|
||||||
|
number | undefined
|
||||||
|
>()
|
||||||
|
|
||||||
|
const handleStepMove = (event: MouseEvent) => {
|
||||||
|
if (!draggedChoiceItem) return
|
||||||
|
const { clientX, clientY } = event
|
||||||
|
setPosition({
|
||||||
|
...position,
|
||||||
|
x: clientX - relativeCoordinates.x,
|
||||||
|
y: clientY - relativeCoordinates.y,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
useEventListener('mousemove', handleStepMove)
|
||||||
|
|
||||||
|
const handleMouseUp = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (expandedPlaceholderIndex === undefined || !draggedChoiceItem) return
|
||||||
|
e.stopPropagation()
|
||||||
|
setMouseOverBlockId(undefined)
|
||||||
|
setExpandedPlaceholderIndex(undefined)
|
||||||
|
setDraggedChoiceItem(undefined)
|
||||||
|
createChoiceItem(draggedChoiceItem, expandedPlaceholderIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStepMouseDown = (
|
||||||
|
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
|
||||||
|
item: ChoiceItem
|
||||||
|
) => {
|
||||||
|
setPosition(absolute)
|
||||||
|
setRelativeCoordinates(relative)
|
||||||
|
setMouseOverBlockId(typebot?.steps.byId[item.stepId].blockId)
|
||||||
|
setDraggedChoiceItem(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseOnTopOfStep = (stepIndex: number) => {
|
||||||
|
if (!draggedChoiceItem) return
|
||||||
|
setExpandedPlaceholderIndex(stepIndex === 0 ? 0 : stepIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseOnBottomOfStep = (stepIndex: number) => {
|
||||||
|
if (!draggedChoiceItem) return
|
||||||
|
setExpandedPlaceholderIndex(stepIndex + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopPropagating = (e: React.MouseEvent) => e.stopPropagation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
flex={1}
|
||||||
|
spacing={1}
|
||||||
|
onMouseUpCapture={handleMouseUp}
|
||||||
|
onClick={stopPropagating}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
h={expandedPlaceholderIndex === 0 ? '50px' : '2px'}
|
||||||
|
bgColor={'gray.400'}
|
||||||
|
visibility={showSortPlaceholders ? 'visible' : 'hidden'}
|
||||||
|
rounded="lg"
|
||||||
|
transition={showSortPlaceholders ? 'height 200ms' : 'none'}
|
||||||
|
/>
|
||||||
|
{step.options.itemIds.map((itemId, idx) => (
|
||||||
|
<Stack key={itemId} spacing={1}>
|
||||||
|
{typebot?.choiceItems.byId[itemId] && (
|
||||||
|
<ChoiceItemNode
|
||||||
|
item={typebot?.choiceItems.byId[itemId]}
|
||||||
|
onMouseMoveTopOfElement={() => handleMouseOnTopOfStep(idx)}
|
||||||
|
onMouseMoveBottomOfElement={() => {
|
||||||
|
handleMouseOnBottomOfStep(idx)
|
||||||
|
}}
|
||||||
|
onMouseDown={handleStepMouseDown}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Flex
|
||||||
|
h={
|
||||||
|
showSortPlaceholders && expandedPlaceholderIndex === idx + 1
|
||||||
|
? '50px'
|
||||||
|
: '2px'
|
||||||
|
}
|
||||||
|
bgColor={'gray.400'}
|
||||||
|
visibility={showSortPlaceholders ? 'visible' : 'hidden'}
|
||||||
|
rounded="lg"
|
||||||
|
transition={showSortPlaceholders ? 'height 200ms' : 'none'}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
<Stack>
|
||||||
|
<Flex
|
||||||
|
px="4"
|
||||||
|
py="2"
|
||||||
|
bgColor="gray.200"
|
||||||
|
borderWidth="2px"
|
||||||
|
rounded="md"
|
||||||
|
pos="relative"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<Text>Default</Text>
|
||||||
|
<SourceEndpoint
|
||||||
|
source={{
|
||||||
|
blockId: step.blockId,
|
||||||
|
stepId: step.id,
|
||||||
|
}}
|
||||||
|
pos="absolute"
|
||||||
|
right="15px"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{draggedChoiceItem && draggedChoiceItem.stepId === step.id && (
|
||||||
|
<Portal>
|
||||||
|
<ChoiceItemNodeOverlay
|
||||||
|
item={draggedChoiceItem}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
pos="fixed"
|
||||||
|
top="0"
|
||||||
|
left="0"
|
||||||
|
style={{
|
||||||
|
transform: `translate(${position.x}px, ${position.y}px) rotate(-2deg)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Portal>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ChoiceItemsList as ChoiceInputStepNodeContent } from './ChoiceItemsList'
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
import { PopoverContent, PopoverArrow, PopoverBody } from '@chakra-ui/react'
|
import { PopoverContent, PopoverArrow, PopoverBody } from '@chakra-ui/react'
|
||||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||||
import { InputStepType, Step, TextInputOptions } from 'models'
|
import {
|
||||||
|
ChoiceInputOptions,
|
||||||
|
InputStep,
|
||||||
|
InputStepType,
|
||||||
|
TextInputOptions,
|
||||||
|
} from 'models'
|
||||||
import {
|
import {
|
||||||
TextInputSettingsBody,
|
TextInputSettingsBody,
|
||||||
NumberInputSettingsBody,
|
NumberInputSettingsBody,
|
||||||
@@ -8,10 +13,11 @@ import {
|
|||||||
UrlInputSettingsBody,
|
UrlInputSettingsBody,
|
||||||
DateInputSettingsBody,
|
DateInputSettingsBody,
|
||||||
} from './bodies'
|
} from './bodies'
|
||||||
|
import { ChoiceInputSettingsBody } from './bodies/ChoiceInputSettingsBody'
|
||||||
import { PhoneNumberSettingsBody } from './bodies/PhoneNumberSettingsBody'
|
import { PhoneNumberSettingsBody } from './bodies/PhoneNumberSettingsBody'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
step: Step
|
step: InputStep
|
||||||
}
|
}
|
||||||
export const SettingsPopoverContent = ({ step }: Props) => {
|
export const SettingsPopoverContent = ({ step }: Props) => {
|
||||||
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()
|
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()
|
||||||
@@ -28,8 +34,9 @@ export const SettingsPopoverContent = ({ step }: Props) => {
|
|||||||
|
|
||||||
const SettingsPopoverBodyContent = ({ step }: Props) => {
|
const SettingsPopoverBodyContent = ({ step }: Props) => {
|
||||||
const { updateStep } = useTypebot()
|
const { updateStep } = useTypebot()
|
||||||
const handleOptionsChange = (options: TextInputOptions) =>
|
const handleOptionsChange = (
|
||||||
updateStep(step.id, { options } as Partial<Step>)
|
options: TextInputOptions | ChoiceInputOptions
|
||||||
|
) => updateStep(step.id, { options } as Partial<InputStep>)
|
||||||
|
|
||||||
switch (step.type) {
|
switch (step.type) {
|
||||||
case InputStepType.TEXT: {
|
case InputStepType.TEXT: {
|
||||||
@@ -80,6 +87,14 @@ const SettingsPopoverBodyContent = ({ step }: Props) => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
case InputStepType.CHOICE: {
|
||||||
|
return (
|
||||||
|
<ChoiceInputSettingsBody
|
||||||
|
options={step.options}
|
||||||
|
onOptionsChange={handleOptionsChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||||
|
import { DebouncedInput } from 'components/shared/DebouncedInput'
|
||||||
|
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||||
|
import { ChoiceInputOptions } from 'models'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
type ChoiceInputSettingsBodyProps = {
|
||||||
|
options?: ChoiceInputOptions
|
||||||
|
onOptionsChange: (options: ChoiceInputOptions) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChoiceInputSettingsBody = ({
|
||||||
|
options,
|
||||||
|
onOptionsChange,
|
||||||
|
}: ChoiceInputSettingsBodyProps) => {
|
||||||
|
const handleIsMultipleChange = (isMultipleChoice: boolean) =>
|
||||||
|
options && onOptionsChange({ ...options, isMultipleChoice })
|
||||||
|
const handleButtonLabelChange = (buttonLabel: string) =>
|
||||||
|
options && onOptionsChange({ ...options, buttonLabel })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<SwitchWithLabel
|
||||||
|
id={'is-multiple'}
|
||||||
|
label={'Multiple choice?'}
|
||||||
|
initialValue={options?.isMultipleChoice ?? false}
|
||||||
|
onCheckChange={handleIsMultipleChange}
|
||||||
|
/>
|
||||||
|
{options?.isMultipleChoice && (
|
||||||
|
<Stack>
|
||||||
|
<FormLabel mb="0" htmlFor="send">
|
||||||
|
Button label:
|
||||||
|
</FormLabel>
|
||||||
|
<DebouncedInput
|
||||||
|
id="send"
|
||||||
|
initialValue={options?.buttonLabel ?? 'Send'}
|
||||||
|
delay={100}
|
||||||
|
onChange={handleButtonLabelChange}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
import { Box, BoxProps } from '@chakra-ui/react'
|
import { Box, BoxProps } from '@chakra-ui/react'
|
||||||
|
import { ConnectingSourceIds, useGraph } from 'contexts/GraphContext'
|
||||||
import React, { MouseEvent } from 'react'
|
import React, { MouseEvent } from 'react'
|
||||||
|
|
||||||
export const SourceEndpoint = ({
|
export const SourceEndpoint = ({
|
||||||
onConnectionDragStart,
|
source,
|
||||||
...props
|
...props
|
||||||
}: BoxProps & {
|
}: BoxProps & {
|
||||||
onConnectionDragStart?: () => void
|
source: ConnectingSourceIds
|
||||||
}) => {
|
}) => {
|
||||||
|
const { setConnectingIds } = useGraph()
|
||||||
|
|
||||||
const handleMouseDown = (e: MouseEvent<HTMLDivElement>) => {
|
const handleMouseDown = (e: MouseEvent<HTMLDivElement>) => {
|
||||||
if (!onConnectionDragStart) return
|
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onConnectionDragStart()
|
setConnectingIds({ source })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,18 +8,18 @@ import {
|
|||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import React, { useEffect, useMemo, useState } from 'react'
|
import React, { useEffect, useMemo, useState } from 'react'
|
||||||
import { Block, Step } from 'models'
|
import { Block, Step } from 'models'
|
||||||
import { SourceEndpoint } from './SourceEndpoint'
|
|
||||||
import { useGraph } from 'contexts/GraphContext'
|
import { useGraph } from 'contexts/GraphContext'
|
||||||
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
|
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
|
||||||
import { isDefined, isTextBubbleStep } from 'utils'
|
import { isChoiceInput, isDefined, isInputStep, isTextBubbleStep } from 'utils'
|
||||||
import { Coordinates } from '@dnd-kit/core/dist/types'
|
import { Coordinates } from '@dnd-kit/core/dist/types'
|
||||||
import { TextEditor } from './TextEditor/TextEditor'
|
import { TextEditor } from './TextEditor/TextEditor'
|
||||||
import { StepNodeLabel } from './StepNodeLabel'
|
import { StepNodeContent } from './StepNodeContent'
|
||||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
import { ContextMenu } from 'components/shared/ContextMenu'
|
import { ContextMenu } from 'components/shared/ContextMenu'
|
||||||
import { StepNodeContextMenu } from './RightClickMenu'
|
|
||||||
import { SettingsPopoverContent } from './SettingsPopoverContent'
|
import { SettingsPopoverContent } from './SettingsPopoverContent'
|
||||||
import { DraggableStep } from 'contexts/DndContext'
|
import { DraggableStep } from 'contexts/DndContext'
|
||||||
|
import { StepNodeContextMenu } from './StepNodeContextMenu'
|
||||||
|
import { SourceEndpoint } from './SourceEndpoint'
|
||||||
|
|
||||||
export const StepNode = ({
|
export const StepNode = ({
|
||||||
step,
|
step,
|
||||||
@@ -38,7 +38,7 @@ export const StepNode = ({
|
|||||||
) => void
|
) => void
|
||||||
}) => {
|
}) => {
|
||||||
const { setConnectingIds, connectingIds } = useGraph()
|
const { setConnectingIds, connectingIds } = useGraph()
|
||||||
const { deleteStep, typebot } = useTypebot()
|
const { moveStep, typebot } = useTypebot()
|
||||||
const [isConnecting, setIsConnecting] = useState(false)
|
const [isConnecting, setIsConnecting] = useState(false)
|
||||||
const [mouseDownEvent, setMouseDownEvent] =
|
const [mouseDownEvent, setMouseDownEvent] =
|
||||||
useState<{ absolute: Coordinates; relative: Coordinates }>()
|
useState<{ absolute: Coordinates; relative: Coordinates }>()
|
||||||
@@ -69,9 +69,6 @@ export const StepNode = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConnectionDragStart = () =>
|
|
||||||
setConnectingIds({ source: { blockId: step.blockId, stepId: step.id } })
|
|
||||||
|
|
||||||
const handleMouseDown = (e: React.MouseEvent) => {
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
if (!onMouseDown) return
|
if (!onMouseDown) return
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@@ -104,7 +101,7 @@ export const StepNode = ({
|
|||||||
(event.movementX > 0 || event.movementY > 0)
|
(event.movementX > 0 || event.movementY > 0)
|
||||||
if (isMovingAndIsMouseDown && step.type !== 'start') {
|
if (isMovingAndIsMouseDown && step.type !== 'start') {
|
||||||
onMouseDown(mouseDownEvent, step)
|
onMouseDown(mouseDownEvent, step)
|
||||||
deleteStep(step.id)
|
moveStep(step.id)
|
||||||
setMouseDownEvent(undefined)
|
setMouseDownEvent(undefined)
|
||||||
}
|
}
|
||||||
const element = event.currentTarget as HTMLDivElement
|
const element = event.currentTarget as HTMLDivElement
|
||||||
@@ -164,6 +161,7 @@ export const StepNode = ({
|
|||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
onMouseUp={handleMouseUp}
|
onMouseUp={handleMouseUp}
|
||||||
data-testid={`step-${step.id}`}
|
data-testid={`step-${step.id}`}
|
||||||
|
w="full"
|
||||||
>
|
>
|
||||||
{connectedStubPosition === 'left' && (
|
{connectedStubPosition === 'left' && (
|
||||||
<Box
|
<Box
|
||||||
@@ -184,19 +182,24 @@ export const StepNode = ({
|
|||||||
rounded="lg"
|
rounded="lg"
|
||||||
cursor={'pointer'}
|
cursor={'pointer'}
|
||||||
bgColor="white"
|
bgColor="white"
|
||||||
|
align="flex-start"
|
||||||
>
|
>
|
||||||
<StepIcon type={step.type} />
|
<StepIcon type={step.type} mt="1" />
|
||||||
<StepNodeLabel {...step} />
|
<StepNodeContent step={step} />
|
||||||
{isConnectable && (
|
{isConnectable && !isChoiceInput(step) && (
|
||||||
<SourceEndpoint
|
<SourceEndpoint
|
||||||
onConnectionDragStart={handleConnectionDragStart}
|
source={{
|
||||||
|
blockId: step.blockId,
|
||||||
|
stepId: step.id,
|
||||||
|
}}
|
||||||
pos="absolute"
|
pos="absolute"
|
||||||
right="20px"
|
right="15px"
|
||||||
|
top="19px"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{isDefined(connectedStubPosition) && (
|
{isDefined(connectedStubPosition) && !isChoiceInput(step) && (
|
||||||
<Box
|
<Box
|
||||||
h="2px"
|
h="2px"
|
||||||
pos="absolute"
|
pos="absolute"
|
||||||
@@ -209,7 +212,7 @@ export const StepNode = ({
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<SettingsPopoverContent step={step} />
|
{isInputStep(step) && <SettingsPopoverContent step={step} />}
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
import { Flex, Text } from '@chakra-ui/react'
|
import { Flex, Text } from '@chakra-ui/react'
|
||||||
import { Step, StartStep, BubbleStepType, InputStepType } from 'models'
|
import { Step, StartStep, BubbleStepType, InputStepType } from 'models'
|
||||||
|
import { ChoiceItemsList } from './ChoiceInputStepNode/ChoiceItemsList'
|
||||||
|
|
||||||
export const StepNodeLabel = (props: Step | StartStep) => {
|
type Props = {
|
||||||
switch (props.type) {
|
step: Step | StartStep
|
||||||
|
isConnectable?: boolean
|
||||||
|
}
|
||||||
|
export const StepNodeContent = ({ step }: Props) => {
|
||||||
|
switch (step.type) {
|
||||||
case BubbleStepType.TEXT: {
|
case BubbleStepType.TEXT: {
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
flexDir={'column'}
|
flexDir={'column'}
|
||||||
opacity={props.content.html === '' ? '0.5' : '1'}
|
opacity={step.content.html === '' ? '0.5' : '1'}
|
||||||
className="slate-html-container"
|
className="slate-html-container"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html:
|
__html:
|
||||||
props.content.html === ''
|
step.content.html === ''
|
||||||
? `<p>Click to edit...</p>`
|
? `<p>Click to edit...</p>`
|
||||||
: props.content.html,
|
: step.content.html,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -21,47 +26,50 @@ export const StepNodeLabel = (props: Step | StartStep) => {
|
|||||||
case InputStepType.TEXT: {
|
case InputStepType.TEXT: {
|
||||||
return (
|
return (
|
||||||
<Text color={'gray.500'}>
|
<Text color={'gray.500'}>
|
||||||
{props.options?.labels?.placeholder ?? 'Type your answer...'}
|
{step.options?.labels?.placeholder ?? 'Type your answer...'}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case InputStepType.NUMBER: {
|
case InputStepType.NUMBER: {
|
||||||
return (
|
return (
|
||||||
<Text color={'gray.500'}>
|
<Text color={'gray.500'}>
|
||||||
{props.options?.labels?.placeholder ?? 'Type your answer...'}
|
{step.options?.labels?.placeholder ?? 'Type your answer...'}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case InputStepType.EMAIL: {
|
case InputStepType.EMAIL: {
|
||||||
return (
|
return (
|
||||||
<Text color={'gray.500'}>
|
<Text color={'gray.500'}>
|
||||||
{props.options?.labels?.placeholder ?? 'Type your email...'}
|
{step.options?.labels?.placeholder ?? 'Type your email...'}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case InputStepType.URL: {
|
case InputStepType.URL: {
|
||||||
return (
|
return (
|
||||||
<Text color={'gray.500'}>
|
<Text color={'gray.500'}>
|
||||||
{props.options?.labels?.placeholder ?? 'Type your URL...'}
|
{step.options?.labels?.placeholder ?? 'Type your URL...'}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case InputStepType.DATE: {
|
case InputStepType.DATE: {
|
||||||
return (
|
return (
|
||||||
<Text color={'gray.500'}>
|
<Text color={'gray.500'}>
|
||||||
{props.options?.labels?.from ?? 'Pick a date...'}
|
{step.options?.labels?.from ?? 'Pick a date...'}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case InputStepType.PHONE: {
|
case InputStepType.PHONE: {
|
||||||
return (
|
return (
|
||||||
<Text color={'gray.500'}>
|
<Text color={'gray.500'}>
|
||||||
{props.options?.labels?.placeholder ?? 'Your phone number...'}
|
{step.options?.labels?.placeholder ?? 'Your phone number...'}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
case InputStepType.CHOICE: {
|
||||||
|
return <ChoiceItemsList step={step} />
|
||||||
|
}
|
||||||
case 'start': {
|
case 'start': {
|
||||||
return <Text>{props.label}</Text>
|
return <Text>{step.label}</Text>
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return <Text>No input</Text>
|
return <Text>No input</Text>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { StackProps, HStack } from '@chakra-ui/react'
|
import { StackProps, HStack } from '@chakra-ui/react'
|
||||||
import { StartStep, Step } from 'models'
|
import { StartStep, Step } from 'models'
|
||||||
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
|
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
|
||||||
import { StepNodeLabel } from './StepNodeLabel'
|
import { StepNodeContent } from './StepNodeContent'
|
||||||
|
|
||||||
export const StepNodeOverlay = ({
|
export const StepNodeOverlay = ({
|
||||||
step,
|
step,
|
||||||
@@ -19,7 +19,7 @@ export const StepNodeOverlay = ({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<StepIcon type={step.type} />
|
<StepIcon type={step.type} />
|
||||||
<StepNodeLabel {...step} />
|
<StepNodeContent step={step} />
|
||||||
</HStack>
|
</HStack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,33 @@ import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react'
|
|||||||
import { Step, Table } from 'models'
|
import { Step, Table } from 'models'
|
||||||
import { DraggableStep, useDnd } from 'contexts/DndContext'
|
import { DraggableStep, useDnd } from 'contexts/DndContext'
|
||||||
import { Coordinates } from 'contexts/GraphContext'
|
import { Coordinates } from 'contexts/GraphContext'
|
||||||
import { useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { StepNode, StepNodeOverlay } from './StepNode'
|
import { StepNode, StepNodeOverlay } from './StepNode'
|
||||||
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
|
|
||||||
export const StepsList = ({
|
export const StepsList = ({
|
||||||
blockId,
|
blockId,
|
||||||
steps,
|
steps,
|
||||||
showSortPlaceholders,
|
|
||||||
onMouseUp,
|
|
||||||
}: {
|
}: {
|
||||||
blockId: string
|
blockId: string
|
||||||
steps: Table<Step>
|
steps: Table<Step>
|
||||||
showSortPlaceholders: boolean
|
|
||||||
onMouseUp: (index: number) => void
|
|
||||||
}) => {
|
}) => {
|
||||||
const { draggedStep, setDraggedStep, draggedStepType } = useDnd()
|
const {
|
||||||
|
draggedStep,
|
||||||
|
setDraggedStep,
|
||||||
|
draggedStepType,
|
||||||
|
mouseOverBlockId,
|
||||||
|
setDraggedStepType,
|
||||||
|
setMouseOverBlockId,
|
||||||
|
} = useDnd()
|
||||||
|
const { createStep } = useTypebot()
|
||||||
const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState<
|
const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState<
|
||||||
number | undefined
|
number | undefined
|
||||||
>()
|
>()
|
||||||
|
const showSortPlaceholders = useMemo(
|
||||||
|
() => mouseOverBlockId === blockId && (draggedStep || draggedStepType),
|
||||||
|
[mouseOverBlockId, blockId, draggedStep, draggedStepType]
|
||||||
|
)
|
||||||
const [position, setPosition] = useState({
|
const [position, setPosition] = useState({
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
@@ -48,8 +57,16 @@ export const StepsList = ({
|
|||||||
const handleMouseUp = (e: React.MouseEvent<HTMLDivElement>) => {
|
const handleMouseUp = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
if (expandedPlaceholderIndex === undefined) return
|
if (expandedPlaceholderIndex === undefined) return
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
setMouseOverBlockId(undefined)
|
||||||
setExpandedPlaceholderIndex(undefined)
|
setExpandedPlaceholderIndex(undefined)
|
||||||
onMouseUp(expandedPlaceholderIndex)
|
if (draggedStepType) {
|
||||||
|
createStep(blockId, draggedStepType, expandedPlaceholderIndex)
|
||||||
|
setDraggedStepType(undefined)
|
||||||
|
}
|
||||||
|
if (draggedStep) {
|
||||||
|
createStep(blockId, draggedStep, expandedPlaceholderIndex)
|
||||||
|
setDraggedStep(undefined)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleStepMouseDown = (
|
const handleStepMouseDown = (
|
||||||
@@ -58,6 +75,7 @@ export const StepsList = ({
|
|||||||
) => {
|
) => {
|
||||||
setPosition(absolute)
|
setPosition(absolute)
|
||||||
setRelativeCoordinates(relative)
|
setRelativeCoordinates(relative)
|
||||||
|
setMouseOverBlockId(blockId)
|
||||||
setDraggedStep(step)
|
setDraggedStep(step)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +136,6 @@ export const StepsList = ({
|
|||||||
<Portal>
|
<Portal>
|
||||||
<StepNodeOverlay
|
<StepNodeOverlay
|
||||||
step={draggedStep}
|
step={draggedStep}
|
||||||
onMouseUp={handleMouseUp}
|
|
||||||
pos="fixed"
|
pos="fixed"
|
||||||
top="0"
|
top="0"
|
||||||
left="0"
|
left="0"
|
||||||
|
|||||||
@@ -1,24 +1,17 @@
|
|||||||
import { useEventListener } from '@chakra-ui/hooks'
|
import { useEventListener } from '@chakra-ui/hooks'
|
||||||
import { Coordinates } from '@dnd-kit/core/dist/types'
|
|
||||||
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
|
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
|
||||||
import {
|
import { useGraph, ConnectingIds } from 'contexts/GraphContext'
|
||||||
blockWidth,
|
|
||||||
firstStepOffsetY,
|
|
||||||
spaceBetweenSteps,
|
|
||||||
stubLength,
|
|
||||||
useGraph,
|
|
||||||
} from 'contexts/GraphContext'
|
|
||||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||||
|
import { Target } from 'models'
|
||||||
import React, { useMemo, useState } from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
computeFlowChartConnectorPath,
|
computeDrawingConnectedPath,
|
||||||
getAnchorsPosition,
|
computeDrawingPathToMouse,
|
||||||
} from 'services/graph'
|
} from 'services/graph'
|
||||||
import { roundCorners } from 'svg-round-corners'
|
|
||||||
|
|
||||||
export const DrawingEdge = () => {
|
export const DrawingEdge = () => {
|
||||||
const { graphPosition, setConnectingIds, connectingIds } = useGraph()
|
const { graphPosition, setConnectingIds, connectingIds } = useGraph()
|
||||||
const { typebot, updateStep } = useTypebot()
|
const { typebot, updateStep, updateChoiceItem } = useTypebot()
|
||||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
|
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
|
||||||
|
|
||||||
const sourceBlock = useMemo(
|
const sourceBlock = useMemo(
|
||||||
@@ -28,33 +21,21 @@ export const DrawingEdge = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const path = useMemo(() => {
|
const path = useMemo(() => {
|
||||||
if (!sourceBlock || !typebot) return ``
|
if (!sourceBlock || !typebot || !connectingIds) return ``
|
||||||
if (connectingIds?.target) {
|
|
||||||
const targetedBlock = typebot?.blocks.byId[connectingIds.target.blockId]
|
return connectingIds?.target
|
||||||
const targetedStepIndex = connectingIds.target.stepId
|
? computeDrawingConnectedPath(
|
||||||
? targetedBlock.stepIds.findIndex(
|
connectingIds as Omit<ConnectingIds, 'target'> & { target: Target },
|
||||||
(stepId) => stepId === connectingIds.target?.stepId
|
sourceBlock,
|
||||||
)
|
typebot
|
||||||
: undefined
|
)
|
||||||
const anchorsPosition = getAnchorsPosition(
|
: computeDrawingPathToMouse(
|
||||||
sourceBlock,
|
sourceBlock,
|
||||||
targetedBlock,
|
connectingIds,
|
||||||
sourceBlock?.stepIds.findIndex(
|
mousePosition,
|
||||||
(stepId) => stepId === connectingIds?.source.stepId
|
typebot.steps
|
||||||
),
|
)
|
||||||
targetedStepIndex
|
}, [sourceBlock, typebot, connectingIds, mousePosition])
|
||||||
)
|
|
||||||
return computeFlowChartConnectorPath(anchorsPosition)
|
|
||||||
}
|
|
||||||
return computeConnectingEdgePath(
|
|
||||||
sourceBlock?.graphCoordinates,
|
|
||||||
mousePosition,
|
|
||||||
sourceBlock.stepIds.findIndex(
|
|
||||||
(stepId) => stepId === connectingIds?.source.stepId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [sourceBlock, mousePosition])
|
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
setMousePosition({
|
setMousePosition({
|
||||||
@@ -64,11 +45,19 @@ export const DrawingEdge = () => {
|
|||||||
}
|
}
|
||||||
useEventListener('mousemove', handleMouseMove)
|
useEventListener('mousemove', handleMouseMove)
|
||||||
useEventListener('mouseup', () => {
|
useEventListener('mouseup', () => {
|
||||||
if (connectingIds?.target)
|
if (connectingIds?.target) createNewEdge(connectingIds)
|
||||||
updateStep(connectingIds.source.stepId, { target: connectingIds.target })
|
|
||||||
setConnectingIds(null)
|
setConnectingIds(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const createNewEdge = (connectingIds: ConnectingIds) =>
|
||||||
|
connectingIds.source.choiceItemId
|
||||||
|
? updateChoiceItem(connectingIds.source.choiceItemId, {
|
||||||
|
target: connectingIds.target,
|
||||||
|
})
|
||||||
|
: updateStep(connectingIds.source.stepId, {
|
||||||
|
target: connectingIds.target,
|
||||||
|
})
|
||||||
|
|
||||||
if ((mousePosition.x === 0 && mousePosition.y === 0) || !connectingIds)
|
if ((mousePosition.x === 0 && mousePosition.y === 0) || !connectingIds)
|
||||||
return <></>
|
return <></>
|
||||||
return (
|
return (
|
||||||
@@ -81,44 +70,3 @@ export const DrawingEdge = () => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const computeConnectingEdgePath = (
|
|
||||||
blockPosition: Coordinates,
|
|
||||||
mousePosition: Coordinates,
|
|
||||||
stepIndex: number
|
|
||||||
): string => {
|
|
||||||
const sourcePosition = {
|
|
||||||
x:
|
|
||||||
mousePosition.x - blockPosition.x > blockWidth / 2
|
|
||||||
? blockPosition.x + blockWidth - 40
|
|
||||||
: blockPosition.x + 40,
|
|
||||||
y: blockPosition.y + firstStepOffsetY + stepIndex * spaceBetweenSteps,
|
|
||||||
}
|
|
||||||
const sourceType =
|
|
||||||
mousePosition.x - blockPosition.x > blockWidth / 2 ? 'right' : 'left'
|
|
||||||
const segments = computeThreeSegments(
|
|
||||||
sourcePosition,
|
|
||||||
mousePosition,
|
|
||||||
sourceType
|
|
||||||
)
|
|
||||||
return roundCorners(
|
|
||||||
`M${sourcePosition.x},${sourcePosition.y} ${segments}`,
|
|
||||||
10
|
|
||||||
).path
|
|
||||||
}
|
|
||||||
|
|
||||||
const computeThreeSegments = (
|
|
||||||
sourcePosition: Coordinates,
|
|
||||||
targetPosition: Coordinates,
|
|
||||||
sourceType: 'right' | 'left'
|
|
||||||
) => {
|
|
||||||
const segments = []
|
|
||||||
const firstSegmentX =
|
|
||||||
sourceType === 'right'
|
|
||||||
? sourcePosition.x + stubLength + 40
|
|
||||||
: sourcePosition.x - stubLength - 40
|
|
||||||
segments.push(`L${firstSegmentX},${sourcePosition.y}`)
|
|
||||||
segments.push(`L${firstSegmentX},${targetPosition.y}`)
|
|
||||||
segments.push(`L${targetPosition.x},${targetPosition.y}`)
|
|
||||||
return segments.join(' ')
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
|
import { isDefined } from '@udecode/plate-core'
|
||||||
|
import assert from 'assert'
|
||||||
import { Coordinates, useGraph } from 'contexts/GraphContext'
|
import { Coordinates, useGraph } from 'contexts/GraphContext'
|
||||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||||
|
import { ChoiceItem } from 'models'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import {
|
import {
|
||||||
getAnchorsPosition,
|
getAnchorsPosition,
|
||||||
computeFlowChartConnectorPath,
|
computeFlowChartConnectorPath,
|
||||||
|
getSourceChoiceItemIndex,
|
||||||
} from 'services/graph'
|
} from 'services/graph'
|
||||||
|
import { isChoiceInput } from 'utils'
|
||||||
|
|
||||||
export type AnchorsPositionProps = {
|
export type AnchorsPositionProps = {
|
||||||
sourcePosition: Coordinates
|
sourcePosition: Coordinates
|
||||||
@@ -13,7 +18,13 @@ export type AnchorsPositionProps = {
|
|||||||
totalSegments: number
|
totalSegments: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Edge = ({ stepId }: { stepId: string }) => {
|
export const Edge = ({
|
||||||
|
stepId,
|
||||||
|
item,
|
||||||
|
}: {
|
||||||
|
stepId: string
|
||||||
|
item?: ChoiceItem
|
||||||
|
}) => {
|
||||||
const { typebot } = useTypebot()
|
const { typebot } = useTypebot()
|
||||||
const { previewingIds } = useGraph()
|
const { previewingIds } = useGraph()
|
||||||
const step = typebot?.steps.byId[stepId]
|
const step = typebot?.steps.byId[stepId]
|
||||||
@@ -27,29 +38,35 @@ export const Edge = ({ stepId }: { stepId: string }) => {
|
|||||||
const { sourceBlock, targetBlock, targetStepIndex } = useMemo(() => {
|
const { sourceBlock, targetBlock, targetStepIndex } = useMemo(() => {
|
||||||
if (!typebot) return {}
|
if (!typebot) return {}
|
||||||
const step = typebot.steps.byId[stepId]
|
const step = typebot.steps.byId[stepId]
|
||||||
if (!step.target) return {}
|
|
||||||
const sourceBlock = typebot.blocks.byId[step.blockId]
|
const sourceBlock = typebot.blocks.byId[step.blockId]
|
||||||
const targetBlock = typebot.blocks.byId[step.target.blockId]
|
const targetBlockId = item?.target?.blockId ?? step.target?.blockId
|
||||||
const targetStepIndex = step.target.stepId
|
assert(isDefined(targetBlockId))
|
||||||
? targetBlock.stepIds.indexOf(step.target.stepId)
|
const targetBlock = typebot.blocks.byId[targetBlockId]
|
||||||
|
const targetStepId = item?.target?.stepId ?? step.target?.stepId
|
||||||
|
const targetStepIndex = targetStepId
|
||||||
|
? targetBlock.stepIds.indexOf(targetStepId)
|
||||||
: undefined
|
: undefined
|
||||||
return {
|
return {
|
||||||
sourceBlock,
|
sourceBlock,
|
||||||
targetBlock,
|
targetBlock,
|
||||||
targetStepIndex,
|
targetStepIndex,
|
||||||
}
|
}
|
||||||
}, [stepId, typebot])
|
}, [item?.target?.blockId, item?.target?.stepId, stepId, typebot])
|
||||||
|
|
||||||
const path = useMemo(() => {
|
const path = useMemo(() => {
|
||||||
if (!sourceBlock || !targetBlock) return ``
|
if (!sourceBlock || !targetBlock || !step) return ``
|
||||||
const anchorsPosition = getAnchorsPosition(
|
const sourceChoiceItemIndex = isChoiceInput(step)
|
||||||
|
? getSourceChoiceItemIndex(step, item?.id)
|
||||||
|
: undefined
|
||||||
|
const anchorsPosition = getAnchorsPosition({
|
||||||
sourceBlock,
|
sourceBlock,
|
||||||
targetBlock,
|
targetBlock,
|
||||||
sourceBlock.stepIds.indexOf(stepId),
|
sourceStepIndex: sourceBlock.stepIds.indexOf(stepId),
|
||||||
targetStepIndex
|
targetStepIndex,
|
||||||
)
|
sourceChoiceItemIndex,
|
||||||
|
})
|
||||||
return computeFlowChartConnectorPath(anchorsPosition)
|
return computeFlowChartConnectorPath(anchorsPosition)
|
||||||
}, [sourceBlock, stepId, targetBlock, targetStepIndex])
|
}, [item, sourceBlock, step, stepId, targetBlock, targetStepIndex])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<path
|
<path
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { chakra } from '@chakra-ui/system'
|
import { chakra } from '@chakra-ui/system'
|
||||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||||
|
import { ChoiceItem } from 'models'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { isDefined } from 'utils'
|
import { isDefined, isSingleChoiceInput } from 'utils'
|
||||||
import { DrawingEdge } from './DrawingEdge'
|
import { DrawingEdge } from './DrawingEdge'
|
||||||
import { Edge } from './Edge'
|
import { Edge } from './Edge'
|
||||||
|
|
||||||
@@ -13,6 +14,18 @@ export const Edges = () => {
|
|||||||
isDefined(typebot.steps.byId[stepId].target)
|
isDefined(typebot.steps.byId[stepId].target)
|
||||||
)
|
)
|
||||||
}, [typebot])
|
}, [typebot])
|
||||||
|
const singleChoiceItemsWithTarget: ChoiceItem[] = useMemo(() => {
|
||||||
|
if (!typebot) return []
|
||||||
|
return typebot.choiceItems.allIds
|
||||||
|
.filter(
|
||||||
|
(itemId) =>
|
||||||
|
isDefined(typebot.choiceItems.byId[itemId].target) &&
|
||||||
|
isSingleChoiceInput(
|
||||||
|
typebot.steps.byId[typebot.choiceItems.byId[itemId].stepId]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((itemId) => typebot.choiceItems.byId[itemId])
|
||||||
|
}, [typebot])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<chakra.svg
|
<chakra.svg
|
||||||
@@ -27,6 +40,9 @@ export const Edges = () => {
|
|||||||
{stepIdsWithTarget.map((stepId) => (
|
{stepIdsWithTarget.map((stepId) => (
|
||||||
<Edge key={stepId} stepId={stepId} />
|
<Edge key={stepId} stepId={stepId} />
|
||||||
))}
|
))}
|
||||||
|
{singleChoiceItemsWithTarget.map((item) => (
|
||||||
|
<Edge key={item.id} stepId={item.stepId} item={item} />
|
||||||
|
))}
|
||||||
<marker
|
<marker
|
||||||
id={'arrow'}
|
id={'arrow'}
|
||||||
refX="8"
|
refX="8"
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { BubbleStep, BubbleStepType, InputStep, InputStepType } from 'models'
|
import {
|
||||||
|
BubbleStep,
|
||||||
|
BubbleStepType,
|
||||||
|
ChoiceItem,
|
||||||
|
InputStep,
|
||||||
|
InputStepType,
|
||||||
|
} from 'models'
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
Dispatch,
|
Dispatch,
|
||||||
@@ -16,16 +22,23 @@ const dndContext = createContext<{
|
|||||||
setDraggedStepType: Dispatch<SetStateAction<DraggableStepType | undefined>>
|
setDraggedStepType: Dispatch<SetStateAction<DraggableStepType | undefined>>
|
||||||
draggedStep?: DraggableStep
|
draggedStep?: DraggableStep
|
||||||
setDraggedStep: Dispatch<SetStateAction<DraggableStep | undefined>>
|
setDraggedStep: Dispatch<SetStateAction<DraggableStep | undefined>>
|
||||||
}>({
|
draggedChoiceItem?: ChoiceItem
|
||||||
setDraggedStep: () => console.log("I'm not implemented"),
|
setDraggedChoiceItem: Dispatch<SetStateAction<ChoiceItem | undefined>>
|
||||||
setDraggedStepType: () => console.log("I'm not implemented"),
|
mouseOverBlockId?: string
|
||||||
})
|
setMouseOverBlockId: Dispatch<SetStateAction<string | undefined>>
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
}>({})
|
||||||
|
|
||||||
export const DndContext = ({ children }: { children: ReactNode }) => {
|
export const DndContext = ({ children }: { children: ReactNode }) => {
|
||||||
const [draggedStep, setDraggedStep] = useState<DraggableStep | undefined>()
|
const [draggedStep, setDraggedStep] = useState<DraggableStep>()
|
||||||
const [draggedStepType, setDraggedStepType] = useState<
|
const [draggedStepType, setDraggedStepType] = useState<
|
||||||
DraggableStepType | undefined
|
DraggableStepType | undefined
|
||||||
>()
|
>()
|
||||||
|
const [draggedChoiceItem, setDraggedChoiceItem] = useState<
|
||||||
|
ChoiceItem | undefined
|
||||||
|
>()
|
||||||
|
const [mouseOverBlockId, setMouseOverBlockId] = useState<string>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dndContext.Provider
|
<dndContext.Provider
|
||||||
@@ -34,6 +47,10 @@ export const DndContext = ({ children }: { children: ReactNode }) => {
|
|||||||
setDraggedStep,
|
setDraggedStep,
|
||||||
draggedStepType,
|
draggedStepType,
|
||||||
setDraggedStepType,
|
setDraggedStepType,
|
||||||
|
draggedChoiceItem,
|
||||||
|
setDraggedChoiceItem,
|
||||||
|
mouseOverBlockId,
|
||||||
|
setMouseOverBlockId,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export const blockAnchorsOffset = {
|
|||||||
export const firstStepOffsetY = 88
|
export const firstStepOffsetY = 88
|
||||||
export const spaceBetweenSteps = 62
|
export const spaceBetweenSteps = 62
|
||||||
|
|
||||||
|
export const firstChoiceItemOffsetY = 20
|
||||||
|
|
||||||
export type Coordinates = { x: number; y: number }
|
export type Coordinates = { x: number; y: number }
|
||||||
|
|
||||||
type Position = Coordinates & { scale: number }
|
type Position = Coordinates & { scale: number }
|
||||||
@@ -43,18 +45,24 @@ export type Node = Omit<Block, 'steps'> & {
|
|||||||
|
|
||||||
const graphPositionDefaultValue = { x: 400, y: 100, scale: 1 }
|
const graphPositionDefaultValue = { x: 400, y: 100, scale: 1 }
|
||||||
|
|
||||||
type ConnectingIdsProps = {
|
export type ConnectingIds = {
|
||||||
source: { blockId: string; stepId: string }
|
source: ConnectingSourceIds
|
||||||
target?: Target
|
target?: Target
|
||||||
} | null
|
}
|
||||||
|
|
||||||
|
export type ConnectingSourceIds = {
|
||||||
|
blockId: string
|
||||||
|
stepId: string
|
||||||
|
choiceItemId?: string
|
||||||
|
}
|
||||||
|
|
||||||
type PreviewingIdsProps = { sourceId?: string; targetId?: string }
|
type PreviewingIdsProps = { sourceId?: string; targetId?: string }
|
||||||
|
|
||||||
const graphContext = createContext<{
|
const graphContext = createContext<{
|
||||||
graphPosition: Position
|
graphPosition: Position
|
||||||
setGraphPosition: Dispatch<SetStateAction<Position>>
|
setGraphPosition: Dispatch<SetStateAction<Position>>
|
||||||
connectingIds: ConnectingIdsProps
|
connectingIds: ConnectingIds | null
|
||||||
setConnectingIds: Dispatch<SetStateAction<ConnectingIdsProps>>
|
setConnectingIds: Dispatch<SetStateAction<ConnectingIds | null>>
|
||||||
previewingIds: PreviewingIdsProps
|
previewingIds: PreviewingIdsProps
|
||||||
setPreviewingIds: Dispatch<SetStateAction<PreviewingIdsProps>>
|
setPreviewingIds: Dispatch<SetStateAction<PreviewingIdsProps>>
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
@@ -66,7 +74,7 @@ const graphContext = createContext<{
|
|||||||
|
|
||||||
export const GraphProvider = ({ children }: { children: ReactNode }) => {
|
export const GraphProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
|
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
|
||||||
const [connectingIds, setConnectingIds] = useState<ConnectingIdsProps>(null)
|
const [connectingIds, setConnectingIds] = useState<ConnectingIds | null>(null)
|
||||||
const [previewingIds, setPreviewingIds] = useState<PreviewingIdsProps>({})
|
const [previewingIds, setPreviewingIds] = useState<PreviewingIdsProps>({})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { isDefined } from 'utils'
|
|||||||
import { BlocksActions, blocksActions } from './actions/blocks'
|
import { BlocksActions, blocksActions } from './actions/blocks'
|
||||||
import { useImmer, Updater } from 'use-immer'
|
import { useImmer, Updater } from 'use-immer'
|
||||||
import { stepsAction, StepsActions } from './actions/steps'
|
import { stepsAction, StepsActions } from './actions/steps'
|
||||||
|
import { choiceItemsAction, ChoiceItemsActions } from './actions/choiceItems'
|
||||||
|
|
||||||
type UpdateTypebotPayload = Partial<{
|
type UpdateTypebotPayload = Partial<{
|
||||||
theme: Theme
|
theme: Theme
|
||||||
@@ -46,7 +47,8 @@ const typebotContext = createContext<
|
|||||||
updateTypebot: (updates: UpdateTypebotPayload) => void
|
updateTypebot: (updates: UpdateTypebotPayload) => void
|
||||||
publishTypebot: () => void
|
publishTypebot: () => void
|
||||||
} & BlocksActions &
|
} & BlocksActions &
|
||||||
StepsActions
|
StepsActions &
|
||||||
|
ChoiceItemsActions
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
>({})
|
>({})
|
||||||
@@ -202,6 +204,7 @@ export const TypebotContext = ({
|
|||||||
updateTypebot: updateLocalTypebot,
|
updateTypebot: updateLocalTypebot,
|
||||||
...blocksActions(setLocalTypebot as Updater<Typebot>),
|
...blocksActions(setLocalTypebot as Updater<Typebot>),
|
||||||
...stepsAction(setLocalTypebot as Updater<Typebot>),
|
...stepsAction(setLocalTypebot as Updater<Typebot>),
|
||||||
|
...choiceItemsAction(setLocalTypebot as Updater<Typebot>),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
79
apps/builder/contexts/TypebotContext/actions/choiceItems.ts
Normal file
79
apps/builder/contexts/TypebotContext/actions/choiceItems.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { ChoiceItem, InputStepType, Typebot } from 'models'
|
||||||
|
import { Updater } from 'use-immer'
|
||||||
|
import { WritableDraft } from 'immer/dist/types/types-external'
|
||||||
|
import { generate } from 'short-uuid'
|
||||||
|
import assert from 'assert'
|
||||||
|
|
||||||
|
export type ChoiceItemsActions = {
|
||||||
|
createChoiceItem: (
|
||||||
|
item: ChoiceItem | Pick<ChoiceItem, 'stepId'>,
|
||||||
|
index?: number
|
||||||
|
) => void
|
||||||
|
updateChoiceItem: (
|
||||||
|
itemId: string,
|
||||||
|
updates: Partial<Omit<ChoiceItem, 'id'>>
|
||||||
|
) => void
|
||||||
|
deleteChoiceItem: (itemId: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const choiceItemsAction = (
|
||||||
|
setTypebot: Updater<Typebot>
|
||||||
|
): ChoiceItemsActions => ({
|
||||||
|
createChoiceItem: (
|
||||||
|
item: ChoiceItem | Pick<ChoiceItem, 'stepId'>,
|
||||||
|
index?: number
|
||||||
|
) => {
|
||||||
|
setTypebot((typebot) => {
|
||||||
|
createChoiceItemDraft(typebot, item, index)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateChoiceItem: (
|
||||||
|
itemId: string,
|
||||||
|
updates: Partial<Omit<ChoiceItem, 'id'>>
|
||||||
|
) =>
|
||||||
|
setTypebot((typebot) => {
|
||||||
|
typebot.choiceItems.byId[itemId] = {
|
||||||
|
...typebot.choiceItems.byId[itemId],
|
||||||
|
...updates,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
deleteChoiceItem: (itemId: string) => {
|
||||||
|
setTypebot((typebot) => {
|
||||||
|
removeChoiceItemFromStep(typebot, itemId)
|
||||||
|
deleteChoiceItemDraft(typebot, itemId)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const removeChoiceItemFromStep = (
|
||||||
|
typebot: WritableDraft<Typebot>,
|
||||||
|
itemId: string
|
||||||
|
) => {
|
||||||
|
const containerStepId = typebot.choiceItems.byId[itemId].stepId
|
||||||
|
const step = typebot.steps.byId[containerStepId]
|
||||||
|
assert(step.type === InputStepType.CHOICE)
|
||||||
|
step.options?.itemIds.splice(step.options.itemIds.indexOf(itemId), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteChoiceItemDraft = (
|
||||||
|
typebot: WritableDraft<Typebot>,
|
||||||
|
itemId: string
|
||||||
|
) => {
|
||||||
|
delete typebot.choiceItems.byId[itemId]
|
||||||
|
const index = typebot.choiceItems.allIds.indexOf(itemId)
|
||||||
|
if (index !== -1) typebot.choiceItems.allIds.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createChoiceItemDraft = (
|
||||||
|
typebot: WritableDraft<Typebot>,
|
||||||
|
item: ChoiceItem | Pick<ChoiceItem, 'stepId'>,
|
||||||
|
index?: number
|
||||||
|
) => {
|
||||||
|
const step = typebot.steps.byId[item.stepId]
|
||||||
|
assert(step.type === InputStepType.CHOICE)
|
||||||
|
const newItem: ChoiceItem =
|
||||||
|
'id' in item ? { ...item } : { id: generate(), stepId: item.stepId }
|
||||||
|
typebot.choiceItems.byId[newItem.id] = newItem
|
||||||
|
typebot.choiceItems.allIds.push(newItem.id)
|
||||||
|
step.options.itemIds.splice(index ?? 0, 0, newItem.id)
|
||||||
|
}
|
||||||
@@ -1,8 +1,16 @@
|
|||||||
import { BubbleStepType, InputStepType, Step, Typebot } from 'models'
|
import {
|
||||||
|
BubbleStepType,
|
||||||
|
ChoiceInputStep,
|
||||||
|
InputStepType,
|
||||||
|
Step,
|
||||||
|
Typebot,
|
||||||
|
} from 'models'
|
||||||
import { parseNewStep } from 'services/typebots'
|
import { parseNewStep } from 'services/typebots'
|
||||||
import { Updater } from 'use-immer'
|
import { Updater } from 'use-immer'
|
||||||
import { removeEmptyBlocks } from './blocks'
|
import { removeEmptyBlocks } from './blocks'
|
||||||
import { WritableDraft } from 'immer/dist/types/types-external'
|
import { WritableDraft } from 'immer/dist/types/types-external'
|
||||||
|
import { createChoiceItemDraft, deleteChoiceItemDraft } from './choiceItems'
|
||||||
|
import { isChoiceInput } from 'utils'
|
||||||
|
|
||||||
export type StepsActions = {
|
export type StepsActions = {
|
||||||
createStep: (
|
createStep: (
|
||||||
@@ -14,6 +22,7 @@ export type StepsActions = {
|
|||||||
stepId: string,
|
stepId: string,
|
||||||
updates: Partial<Omit<Step, 'id' | 'type'>>
|
updates: Partial<Omit<Step, 'id' | 'type'>>
|
||||||
) => void
|
) => void
|
||||||
|
moveStep: (stepId: string) => void
|
||||||
deleteStep: (stepId: string) => void
|
deleteStep: (stepId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,11 +41,17 @@ export const stepsAction = (setTypebot: Updater<Typebot>): StepsActions => ({
|
|||||||
setTypebot((typebot) => {
|
setTypebot((typebot) => {
|
||||||
typebot.steps.byId[stepId] = { ...typebot.steps.byId[stepId], ...updates }
|
typebot.steps.byId[stepId] = { ...typebot.steps.byId[stepId], ...updates }
|
||||||
}),
|
}),
|
||||||
deleteStep: (stepId: string) => {
|
moveStep: (stepId: string) => {
|
||||||
setTypebot((typebot) => {
|
setTypebot((typebot) => {
|
||||||
removeStepIdFromBlock(typebot, stepId)
|
removeStepIdFromBlock(typebot, stepId)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteStep: (stepId: string) => {
|
||||||
|
setTypebot((typebot) => {
|
||||||
|
const step = typebot.steps.byId[stepId]
|
||||||
|
if (isChoiceInput(step)) deleteChoiceItemsInsideStep(typebot, step)
|
||||||
|
removeStepIdFromBlock(typebot, stepId)
|
||||||
deleteStepDraft(typebot, stepId)
|
deleteStepDraft(typebot, stepId)
|
||||||
removeEmptyBlocks(typebot)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -45,13 +60,8 @@ const removeStepIdFromBlock = (
|
|||||||
typebot: WritableDraft<Typebot>,
|
typebot: WritableDraft<Typebot>,
|
||||||
stepId: string
|
stepId: string
|
||||||
) => {
|
) => {
|
||||||
const containerBlockId = typebot.blocks.allIds.find((blockId) =>
|
const containerBlock = typebot.blocks.byId[typebot.steps.byId[stepId].blockId]
|
||||||
typebot.blocks.byId[blockId].stepIds.includes(stepId)
|
containerBlock.stepIds.splice(containerBlock.stepIds.indexOf(stepId), 1)
|
||||||
) as string
|
|
||||||
typebot.blocks.byId[containerBlockId].stepIds.splice(
|
|
||||||
typebot.blocks.byId[containerBlockId].stepIds.indexOf(stepId),
|
|
||||||
1
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteStepDraft = (
|
export const deleteStepDraft = (
|
||||||
@@ -74,6 +84,16 @@ export const createStepDraft = (
|
|||||||
? parseNewStep(step, blockId)
|
? parseNewStep(step, blockId)
|
||||||
: { ...step, blockId }
|
: { ...step, blockId }
|
||||||
typebot.steps.byId[newStep.id] = newStep
|
typebot.steps.byId[newStep.id] = newStep
|
||||||
|
if (isChoiceInput(newStep) && newStep.options.itemIds.length === 0)
|
||||||
|
createChoiceItemDraft(typebot, { stepId: newStep.id })
|
||||||
typebot.steps.allIds.push(newStep.id)
|
typebot.steps.allIds.push(newStep.id)
|
||||||
typebot.blocks.byId[blockId].stepIds.splice(index ?? 0, 0, newStep.id)
|
typebot.blocks.byId[blockId].stepIds.splice(index ?? 0, 0, newStep.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteChoiceItemsInsideStep = (
|
||||||
|
typebot: WritableDraft<Typebot>,
|
||||||
|
step: ChoiceInputStep
|
||||||
|
) =>
|
||||||
|
step.options?.itemIds.forEach((itemId) =>
|
||||||
|
deleteChoiceItemDraft(typebot, itemId)
|
||||||
|
)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { TypebotContext } from './TypebotContext'
|
export { TypebotContext, useTypebot } from './TypebotContext'
|
||||||
|
|||||||
@@ -127,4 +127,5 @@ const parseTypebotToPublicTypebot = (
|
|||||||
theme: typebot.theme,
|
theme: typebot.theme,
|
||||||
settings: typebot.settings,
|
settings: typebot.settings,
|
||||||
publicId: typebot.publicId,
|
publicId: typebot.publicId,
|
||||||
|
choiceItems: typebot.choiceItems,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Typebot,
|
Typebot,
|
||||||
Table,
|
Table,
|
||||||
Step,
|
Step,
|
||||||
|
ChoiceItem,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
|
|
||||||
export const parseTestTypebot = ({
|
export const parseTestTypebot = ({
|
||||||
@@ -14,12 +15,14 @@ export const parseTestTypebot = ({
|
|||||||
name,
|
name,
|
||||||
blocks,
|
blocks,
|
||||||
steps,
|
steps,
|
||||||
|
choiceItems,
|
||||||
}: {
|
}: {
|
||||||
id: string
|
id: string
|
||||||
ownerId: string
|
ownerId: string
|
||||||
name: string
|
name: string
|
||||||
blocks: Table<Block>
|
blocks: Table<Block>
|
||||||
steps: Table<Step>
|
steps: Table<Step>
|
||||||
|
choiceItems?: Table<ChoiceItem>
|
||||||
}): Typebot => {
|
}): Typebot => {
|
||||||
const theme: Theme = {
|
const theme: Theme = {
|
||||||
general: {
|
general: {
|
||||||
@@ -67,6 +70,7 @@ export const parseTestTypebot = ({
|
|||||||
},
|
},
|
||||||
allIds: ['step0', ...steps.allIds],
|
allIds: ['step0', ...steps.allIds],
|
||||||
},
|
},
|
||||||
|
choiceItems: choiceItems ?? { byId: {}, allIds: [] },
|
||||||
publicId: null,
|
publicId: null,
|
||||||
publishedTypebotId: null,
|
publishedTypebotId: null,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
|||||||
@@ -175,7 +175,6 @@ describe('Date input', () => {
|
|||||||
describe('Phone number input', () => {
|
describe('Phone number input', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.task('seed')
|
cy.task('seed')
|
||||||
cy.log(JSON.stringify({ type: InputStepType.PHONE }))
|
|
||||||
createTypebotWithStep({ type: InputStepType.PHONE })
|
createTypebotWithStep({ type: InputStepType.PHONE })
|
||||||
cy.signOut()
|
cy.signOut()
|
||||||
})
|
})
|
||||||
@@ -207,6 +206,53 @@ describe('Phone number input', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Button input', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.task('seed')
|
||||||
|
createTypebotWithStep({ type: InputStepType.CHOICE })
|
||||||
|
cy.signOut()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Can edit choice items', () => {
|
||||||
|
cy.signIn('test2@gmail.com')
|
||||||
|
cy.visit('/typebots/typebot3/edit')
|
||||||
|
cy.findByDisplayValue('Click to edit').type('Item 1{enter}')
|
||||||
|
cy.findByText('Item 1').trigger('mouseover')
|
||||||
|
cy.findByRole('button', { name: 'Add item' }).click()
|
||||||
|
cy.findByDisplayValue('Click to edit').type('Item 2{enter}')
|
||||||
|
cy.findByRole('button', { name: 'Add item' }).click()
|
||||||
|
cy.findByDisplayValue('Click to edit').type('Item 3{enter}')
|
||||||
|
cy.findByText('Item 2').rightclick()
|
||||||
|
cy.findByRole('menuitem', { name: 'Delete' }).click()
|
||||||
|
cy.findByText('Item 2').should('not.exist')
|
||||||
|
cy.findByTestId('step-step1').click({ force: true })
|
||||||
|
cy.findByRole('button', { name: 'Preview' }).click()
|
||||||
|
getIframeBody().findByRole('button', { name: 'Item 3' }).click()
|
||||||
|
getIframeBody().findByRole('button', { name: 'Item 3' }).should('not.exist')
|
||||||
|
getIframeBody().findByText('Item 3')
|
||||||
|
cy.findByRole('button', { name: 'Close' }).click()
|
||||||
|
cy.findByTestId('step-step1').click({ force: true })
|
||||||
|
cy.findByRole('checkbox', { name: 'Multiple choice?' }).check({
|
||||||
|
force: true,
|
||||||
|
})
|
||||||
|
cy.findByRole('textbox', { name: 'Button label:' }).clear().type('Go')
|
||||||
|
cy.wait(200)
|
||||||
|
cy.findByTestId('step-step1').click({ force: true })
|
||||||
|
cy.findByText('Item 1').trigger('mouseover')
|
||||||
|
cy.findByRole('button', { name: 'Add item' }).click()
|
||||||
|
cy.findByDisplayValue('Click to edit').type('Item 2{enter}')
|
||||||
|
cy.findByRole('button', { name: 'Preview' }).click()
|
||||||
|
getIframeBody().findByRole('checkbox', { name: 'Item 3' }).click()
|
||||||
|
getIframeBody().findByRole('checkbox', { name: 'Item 1' }).click()
|
||||||
|
getIframeBody().findByRole('button', { name: 'Go' }).click()
|
||||||
|
getIframeBody().findByText('Item 3, Item 1').should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Single choice targets should work', () => {
|
||||||
|
//TO-DO
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const createTypebotWithStep = (step: Omit<InputStep, 'id' | 'blockId'>) => {
|
const createTypebotWithStep = (step: Omit<InputStep, 'id' | 'blockId'>) => {
|
||||||
cy.task(
|
cy.task(
|
||||||
'createTypebot',
|
'createTypebot',
|
||||||
@@ -216,7 +262,17 @@ const createTypebotWithStep = (step: Omit<InputStep, 'id' | 'blockId'>) => {
|
|||||||
ownerId: 'test2',
|
ownerId: 'test2',
|
||||||
steps: {
|
steps: {
|
||||||
byId: {
|
byId: {
|
||||||
step1: { ...step, id: 'step1', blockId: 'block1' },
|
step1: {
|
||||||
|
...step,
|
||||||
|
id: 'step1',
|
||||||
|
blockId: 'block1',
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
options:
|
||||||
|
step.type === InputStepType.CHOICE
|
||||||
|
? { itemIds: ['item1'] }
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
allIds: ['step1'],
|
allIds: ['step1'],
|
||||||
},
|
},
|
||||||
@@ -231,6 +287,13 @@ const createTypebotWithStep = (step: Omit<InputStep, 'id' | 'blockId'>) => {
|
|||||||
},
|
},
|
||||||
allIds: ['block1'],
|
allIds: ['block1'],
|
||||||
},
|
},
|
||||||
|
choiceItems:
|
||||||
|
step.type === InputStepType.CHOICE
|
||||||
|
? {
|
||||||
|
byId: { item1: { stepId: 'step1', id: 'item1' } },
|
||||||
|
allIds: ['item1'],
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Coordinates } from '@dnd-kit/core/dist/types'
|
import { Coordinates } from '@dnd-kit/core/dist/types'
|
||||||
import { Block } from 'models'
|
import { Block, ChoiceInputStep, Step, Table, Target, Typebot } from 'models'
|
||||||
import { AnchorsPositionProps } from 'components/board/graph/Edges/Edge'
|
import { AnchorsPositionProps } from 'components/board/graph/Edges/Edge'
|
||||||
import {
|
import {
|
||||||
stubLength,
|
stubLength,
|
||||||
@@ -7,9 +7,11 @@ import {
|
|||||||
blockAnchorsOffset,
|
blockAnchorsOffset,
|
||||||
spaceBetweenSteps,
|
spaceBetweenSteps,
|
||||||
firstStepOffsetY,
|
firstStepOffsetY,
|
||||||
|
firstChoiceItemOffsetY,
|
||||||
|
ConnectingIds,
|
||||||
} from 'contexts/GraphContext'
|
} from 'contexts/GraphContext'
|
||||||
import { roundCorners } from 'svg-round-corners'
|
import { roundCorners } from 'svg-round-corners'
|
||||||
import { isDefined } from 'utils'
|
import { isChoiceInput, isDefined } from 'utils'
|
||||||
|
|
||||||
export const computeDropOffPath = (
|
export const computeDropOffPath = (
|
||||||
sourcePosition: Coordinates,
|
sourcePosition: Coordinates,
|
||||||
@@ -25,13 +27,18 @@ export const computeDropOffPath = (
|
|||||||
|
|
||||||
export const computeSourceCoordinates = (
|
export const computeSourceCoordinates = (
|
||||||
sourcePosition: Coordinates,
|
sourcePosition: Coordinates,
|
||||||
sourceStepIndex: number
|
sourceStepIndex: number,
|
||||||
|
sourceChoiceItemIndex?: number
|
||||||
) => ({
|
) => ({
|
||||||
x: sourcePosition.x + blockWidth,
|
x: sourcePosition.x + blockWidth,
|
||||||
y:
|
y:
|
||||||
(sourcePosition.y ?? 0) +
|
(sourcePosition.y ?? 0) +
|
||||||
firstStepOffsetY +
|
firstStepOffsetY +
|
||||||
spaceBetweenSteps * sourceStepIndex,
|
spaceBetweenSteps * sourceStepIndex +
|
||||||
|
(isDefined(sourceChoiceItemIndex)
|
||||||
|
? firstChoiceItemOffsetY +
|
||||||
|
(sourceChoiceItemIndex ?? 0) * spaceBetweenSteps
|
||||||
|
: 0),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const computeFlowChartConnectorPath = ({
|
export const computeFlowChartConnectorPath = ({
|
||||||
@@ -142,12 +149,20 @@ const computeFiveSegments = (
|
|||||||
return segments.join(' ')
|
return segments.join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAnchorsPosition = (
|
type GetAnchorsPositionParams = {
|
||||||
sourceBlock: Block,
|
sourceBlock: Block
|
||||||
targetBlock: Block,
|
targetBlock: Block
|
||||||
sourceStepIndex: number,
|
sourceStepIndex: number
|
||||||
|
sourceChoiceItemIndex?: number
|
||||||
targetStepIndex?: number
|
targetStepIndex?: number
|
||||||
): AnchorsPositionProps => {
|
}
|
||||||
|
export const getAnchorsPosition = ({
|
||||||
|
sourceBlock,
|
||||||
|
targetBlock,
|
||||||
|
sourceStepIndex,
|
||||||
|
sourceChoiceItemIndex,
|
||||||
|
targetStepIndex,
|
||||||
|
}: GetAnchorsPositionParams): AnchorsPositionProps => {
|
||||||
const targetOffsetY = isDefined(targetStepIndex)
|
const targetOffsetY = isDefined(targetStepIndex)
|
||||||
? (targetBlock.graphCoordinates.y ?? 0) +
|
? (targetBlock.graphCoordinates.y ?? 0) +
|
||||||
firstStepOffsetY +
|
firstStepOffsetY +
|
||||||
@@ -156,7 +171,8 @@ export const getAnchorsPosition = (
|
|||||||
|
|
||||||
const sourcePosition = computeSourceCoordinates(
|
const sourcePosition = computeSourceCoordinates(
|
||||||
sourceBlock.graphCoordinates,
|
sourceBlock.graphCoordinates,
|
||||||
sourceStepIndex
|
sourceStepIndex,
|
||||||
|
sourceChoiceItemIndex
|
||||||
)
|
)
|
||||||
let sourceType: 'right' | 'left' = 'right'
|
let sourceType: 'right' | 'left' = 'right'
|
||||||
if (sourceBlock.graphCoordinates.x > targetBlock.graphCoordinates.x) {
|
if (sourceBlock.graphCoordinates.x > targetBlock.graphCoordinates.x) {
|
||||||
@@ -230,3 +246,95 @@ const parseBlockAnchorPosition = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const computeDrawingConnectedPath = (
|
||||||
|
connectingIds: Omit<ConnectingIds, 'target'> & { target: Target },
|
||||||
|
sourceBlock: Block,
|
||||||
|
typebot: Typebot
|
||||||
|
) => {
|
||||||
|
if (!sourceBlock) return ``
|
||||||
|
const targetBlock = typebot.blocks.byId[connectingIds.target.blockId]
|
||||||
|
const targetStepIndex = connectingIds.target.stepId
|
||||||
|
? targetBlock.stepIds.findIndex(
|
||||||
|
(stepId) => stepId === connectingIds.target?.stepId
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const sourceStepIndex = sourceBlock?.stepIds.indexOf(
|
||||||
|
connectingIds?.source.stepId
|
||||||
|
)
|
||||||
|
const sourceStep = typebot.steps.byId[connectingIds?.source.stepId]
|
||||||
|
const sourceChoiceItemIndex = isChoiceInput(sourceStep)
|
||||||
|
? getSourceChoiceItemIndex(sourceStep, connectingIds.source.choiceItemId)
|
||||||
|
: undefined
|
||||||
|
const anchorsPosition = getAnchorsPosition({
|
||||||
|
sourceBlock,
|
||||||
|
targetBlock,
|
||||||
|
sourceStepIndex,
|
||||||
|
sourceChoiceItemIndex,
|
||||||
|
targetStepIndex,
|
||||||
|
})
|
||||||
|
return computeFlowChartConnectorPath(anchorsPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const computeDrawingPathToMouse = (
|
||||||
|
sourceBlock: Block,
|
||||||
|
connectingIds: ConnectingIds,
|
||||||
|
mousePosition: Coordinates,
|
||||||
|
steps: Table<Step>
|
||||||
|
) => {
|
||||||
|
const sourceStep = steps.byId[connectingIds?.source.stepId ?? '']
|
||||||
|
return computeConnectingEdgePath({
|
||||||
|
blockPosition: sourceBlock?.graphCoordinates,
|
||||||
|
mousePosition,
|
||||||
|
stepIndex: sourceBlock.stepIds.findIndex(
|
||||||
|
(stepId) => stepId === connectingIds?.source.stepId
|
||||||
|
),
|
||||||
|
choiceItemIndex: isChoiceInput(sourceStep)
|
||||||
|
? getSourceChoiceItemIndex(sourceStep, connectingIds?.source.choiceItemId)
|
||||||
|
: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const computeConnectingEdgePath = ({
|
||||||
|
blockPosition,
|
||||||
|
mousePosition,
|
||||||
|
stepIndex,
|
||||||
|
choiceItemIndex,
|
||||||
|
}: {
|
||||||
|
blockPosition: Coordinates
|
||||||
|
mousePosition: Coordinates
|
||||||
|
stepIndex: number
|
||||||
|
choiceItemIndex?: number
|
||||||
|
}): string => {
|
||||||
|
const sourcePosition = {
|
||||||
|
x:
|
||||||
|
mousePosition.x - blockPosition.x > blockWidth / 2
|
||||||
|
? blockPosition.x + blockWidth - 40
|
||||||
|
: blockPosition.x + 40,
|
||||||
|
y:
|
||||||
|
blockPosition.y +
|
||||||
|
firstStepOffsetY +
|
||||||
|
stepIndex * spaceBetweenSteps +
|
||||||
|
(isDefined(choiceItemIndex)
|
||||||
|
? firstChoiceItemOffsetY + (choiceItemIndex ?? 0) * spaceBetweenSteps
|
||||||
|
: 0),
|
||||||
|
}
|
||||||
|
const sourceType =
|
||||||
|
mousePosition.x - blockPosition.x > blockWidth / 2 ? 'right' : 'left'
|
||||||
|
const segments = computeThreeSegments(
|
||||||
|
sourcePosition,
|
||||||
|
mousePosition,
|
||||||
|
sourceType
|
||||||
|
)
|
||||||
|
return roundCorners(
|
||||||
|
`M${sourcePosition.x},${sourcePosition.y} ${segments}`,
|
||||||
|
10
|
||||||
|
).path
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSourceChoiceItemIndex = (
|
||||||
|
step: ChoiceInputStep,
|
||||||
|
itemId?: string
|
||||||
|
) =>
|
||||||
|
itemId ? step.options.itemIds.indexOf(itemId) : step.options.itemIds.length
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const parseTypebotToPublicTypebot = (
|
|||||||
theme: typebot.theme,
|
theme: typebot.theme,
|
||||||
settings: typebot.settings,
|
settings: typebot.settings,
|
||||||
publicId: typebot.publicId,
|
publicId: typebot.publicId,
|
||||||
|
choiceItems: typebot.choiceItems,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const createPublishedTypebot = async (
|
export const createPublishedTypebot = async (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
InputStep,
|
InputStep,
|
||||||
BubbleStepType,
|
BubbleStepType,
|
||||||
InputStepType,
|
InputStepType,
|
||||||
|
ChoiceInputStep,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import shortId from 'short-uuid'
|
import shortId from 'short-uuid'
|
||||||
import { Typebot } from 'models'
|
import { Typebot } from 'models'
|
||||||
@@ -122,6 +123,17 @@ export const parseNewStep = (
|
|||||||
...textStep,
|
...textStep,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case InputStepType.CHOICE: {
|
||||||
|
const choiceInput: Pick<ChoiceInputStep, 'type' | 'options'> = {
|
||||||
|
type,
|
||||||
|
options: { itemIds: [] },
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
blockId,
|
||||||
|
...choiceInput,
|
||||||
|
}
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -201,6 +213,7 @@ export const parseNewTypebot = ({
|
|||||||
ownerId,
|
ownerId,
|
||||||
blocks: { byId: { [startBlockId]: startBlock }, allIds: [startBlockId] },
|
blocks: { byId: { [startBlockId]: startBlock }, allIds: [startBlockId] },
|
||||||
steps: { byId: { [startStepId]: startStep }, allIds: [startStepId] },
|
steps: { byId: { [startStepId]: startStep }, allIds: [startStepId] },
|
||||||
|
choiceItems: { byId: {}, allIds: [] },
|
||||||
theme,
|
theme,
|
||||||
settings,
|
settings,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"db": "*",
|
"db": "*",
|
||||||
"fast-equals": "^2.0.4",
|
"fast-equals": "^2.0.4",
|
||||||
"models": "*",
|
"models": "*",
|
||||||
"react-frame-component": "^5.2.1",
|
"react-frame-component": "5.2.2-alpha.0",
|
||||||
"react-phone-number-input": "^3.1.44",
|
"react-phone-number-input": "^3.1.44",
|
||||||
"react-scroll": "^1.8.4",
|
"react-scroll": "^1.8.4",
|
||||||
"react-transition-group": "^4.4.2",
|
"react-transition-group": "^4.4.2",
|
||||||
|
|||||||
@@ -4,18 +4,21 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
|||||||
import { ChatStep } from './ChatStep'
|
import { ChatStep } from './ChatStep'
|
||||||
import { AvatarSideContainer } from './AvatarSideContainer'
|
import { AvatarSideContainer } from './AvatarSideContainer'
|
||||||
import { HostAvatarsContext } from '../../contexts/HostAvatarsContext'
|
import { HostAvatarsContext } from '../../contexts/HostAvatarsContext'
|
||||||
import { Step, Table } from 'models'
|
import { ChoiceInputStep, Step } from 'models'
|
||||||
|
import { useTypebot } from '../../contexts/TypebotContext'
|
||||||
|
import { isChoiceInput } from 'utils'
|
||||||
|
|
||||||
type ChatBlockProps = {
|
type ChatBlockProps = {
|
||||||
steps: Table<Step>
|
stepIds: string[]
|
||||||
onBlockEnd: (nextBlockId?: string) => void
|
onBlockEnd: (nextBlockId?: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatBlock = ({ steps, onBlockEnd }: ChatBlockProps) => {
|
export const ChatBlock = ({ stepIds, onBlockEnd }: ChatBlockProps) => {
|
||||||
|
const { typebot } = useTypebot()
|
||||||
const [displayedSteps, setDisplayedSteps] = useState<Step[]>([])
|
const [displayedSteps, setDisplayedSteps] = useState<Step[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDisplayedSteps([steps.byId[steps.allIds[0]]])
|
setDisplayedSteps([typebot.steps.byId[stepIds[0]]])
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -29,17 +32,36 @@ export const ChatBlock = ({ steps, onBlockEnd }: ChatBlockProps) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayNextStep = () => {
|
const displayNextStep = (answerContent?: string) => {
|
||||||
const currentStep = [...displayedSteps].pop()
|
const currentStep = [...displayedSteps].pop()
|
||||||
|
if (!currentStep) throw new Error('currentStep should exist')
|
||||||
|
const isSingleChoiceStep =
|
||||||
|
isChoiceInput(currentStep) && !currentStep.options.isMultipleChoice
|
||||||
|
if (isSingleChoiceStep)
|
||||||
|
return onBlockEnd(getSingleChoiceTargetId(currentStep, answerContent))
|
||||||
if (
|
if (
|
||||||
currentStep?.target?.blockId ||
|
currentStep?.target?.blockId ||
|
||||||
displayedSteps.length === steps.allIds.length
|
displayedSteps.length === stepIds.length
|
||||||
)
|
)
|
||||||
return onBlockEnd(currentStep?.target?.blockId)
|
return onBlockEnd(currentStep?.target?.blockId)
|
||||||
const nextStep = steps.byId[displayedSteps.length]
|
const nextStep = typebot.steps.byId[stepIds[displayedSteps.length]]
|
||||||
if (nextStep) setDisplayedSteps([...displayedSteps, nextStep])
|
if (nextStep) setDisplayedSteps([...displayedSteps, nextStep])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getSingleChoiceTargetId = (
|
||||||
|
currentStep: ChoiceInputStep,
|
||||||
|
answerContent?: string
|
||||||
|
) => {
|
||||||
|
const itemId = currentStep.options.itemIds.find(
|
||||||
|
(itemId) => typebot.choiceItems.byId[itemId].content === answerContent
|
||||||
|
)
|
||||||
|
if (!itemId) throw new Error('itemId should exist')
|
||||||
|
const targetId =
|
||||||
|
typebot.choiceItems.byId[itemId].target?.blockId ??
|
||||||
|
currentStep.target?.blockId
|
||||||
|
return targetId
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<HostAvatarsContext>
|
<HostAvatarsContext>
|
||||||
|
|||||||
@@ -7,19 +7,20 @@ import { HostMessageBubble } from './bubbles/HostMessageBubble'
|
|||||||
import { TextForm } from './inputs/TextForm'
|
import { TextForm } from './inputs/TextForm'
|
||||||
import { isInputStep, isTextBubbleStep } from 'utils'
|
import { isInputStep, isTextBubbleStep } from 'utils'
|
||||||
import { DateForm } from './inputs/DateForm'
|
import { DateForm } from './inputs/DateForm'
|
||||||
|
import { ChoiceForm } from './inputs/ChoiceForm'
|
||||||
|
|
||||||
export const ChatStep = ({
|
export const ChatStep = ({
|
||||||
step,
|
step,
|
||||||
onTransitionEnd,
|
onTransitionEnd,
|
||||||
}: {
|
}: {
|
||||||
step: Step
|
step: Step
|
||||||
onTransitionEnd: () => void
|
onTransitionEnd: (answerContent?: string) => void
|
||||||
}) => {
|
}) => {
|
||||||
const { addAnswer } = useAnswers()
|
const { addAnswer } = useAnswers()
|
||||||
|
|
||||||
const handleInputSubmit = (content: string) => {
|
const handleInputSubmit = (content: string) => {
|
||||||
addAnswer({ stepId: step.id, blockId: step.blockId, content })
|
addAnswer({ stepId: step.id, blockId: step.blockId, content })
|
||||||
onTransitionEnd()
|
onTransitionEnd(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextBubbleStep(step))
|
if (isTextBubbleStep(step))
|
||||||
@@ -60,5 +61,7 @@ const InputChatStep = ({
|
|||||||
return <TextForm step={step} onSubmit={handleSubmit} />
|
return <TextForm step={step} onSubmit={handleSubmit} />
|
||||||
case InputStepType.DATE:
|
case InputStepType.DATE:
|
||||||
return <DateForm options={step.options} onSubmit={handleSubmit} />
|
return <DateForm options={step.options} onSubmit={handleSubmit} />
|
||||||
|
case InputStepType.CHOICE:
|
||||||
|
return <ChoiceForm options={step.options} onSubmit={handleSubmit} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { ChoiceInputOptions } from 'models'
|
||||||
|
import React, { useMemo, useState } from 'react'
|
||||||
|
import { filterTable } from 'utils'
|
||||||
|
import { useTypebot } from '../../../../contexts/TypebotContext'
|
||||||
|
import { SendButton } from './SendButton'
|
||||||
|
|
||||||
|
type ChoiceFormProps = {
|
||||||
|
options?: ChoiceInputOptions
|
||||||
|
onSubmit: (value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChoiceForm = ({ options, onSubmit }: ChoiceFormProps) => {
|
||||||
|
const { typebot } = useTypebot()
|
||||||
|
const items = useMemo(
|
||||||
|
() => filterTable(options?.itemIds ?? [], typebot.choiceItems),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||||
|
|
||||||
|
const handleClick = (itemId: string) => (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (options?.isMultipleChoice) toggleSelectedItemId(itemId)
|
||||||
|
else onSubmit(items.byId[itemId].content ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSelectedItemId = (itemId: string) => {
|
||||||
|
const existingIndex = selectedIds.indexOf(itemId)
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
selectedIds.splice(existingIndex, 1)
|
||||||
|
setSelectedIds([...selectedIds])
|
||||||
|
} else {
|
||||||
|
setSelectedIds([...selectedIds, itemId])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () =>
|
||||||
|
onSubmit(selectedIds.map((itemId) => items.byId[itemId].content).join(', '))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="flex flex-col" onSubmit={handleSubmit}>
|
||||||
|
<div className="flex flex-wrap">
|
||||||
|
{options?.itemIds.map((itemId) => (
|
||||||
|
<button
|
||||||
|
role={options?.isMultipleChoice ? 'checkbox' : 'button'}
|
||||||
|
onClick={handleClick(itemId)}
|
||||||
|
className={
|
||||||
|
'py-2 px-4 font-semibold rounded-md transition-all filter hover:brightness-90 active:brightness-75 duration-100 focus:outline-none mr-2 mb-2 typebot-button ' +
|
||||||
|
(selectedIds.includes(itemId) || !options?.isMultipleChoice
|
||||||
|
? 'active'
|
||||||
|
: '')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{items.byId[itemId].content}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
{selectedIds.length > 0 && (
|
||||||
|
<SendButton label={options?.buttonLabel ?? 'Send'} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -62,6 +62,7 @@ export const DateForm = ({
|
|||||||
<SendButton
|
<SendButton
|
||||||
label={labels?.button ?? 'Send'}
|
label={labels?.button ?? 'Send'}
|
||||||
isDisabled={inputValues.to === '' && inputValues.from === ''}
|
isDisabled={inputValues.to === '' && inputValues.from === ''}
|
||||||
|
className="my-2 ml-2"
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { SendIcon } from '../../../../assets/icons'
|
|||||||
|
|
||||||
type SendButtonProps = {
|
type SendButtonProps = {
|
||||||
label: string
|
label: string
|
||||||
isDisabled: boolean
|
isDisabled?: boolean
|
||||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>
|
} & React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||||
|
|
||||||
export const SendButton = ({
|
export const SendButton = ({
|
||||||
@@ -14,11 +14,12 @@ export const SendButton = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={
|
|
||||||
'my-2 ml-2 py-2 px-4 font-semibold rounded-md text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 typebot-button active'
|
|
||||||
}
|
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
{...props}
|
{...props}
|
||||||
|
className={
|
||||||
|
'py-2 px-4 font-semibold rounded-md text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 typebot-button active ' +
|
||||||
|
props.className
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span className="hidden xs:flex">{label}</span>
|
<span className="hidden xs:flex">{label}</span>
|
||||||
<SendIcon className="send-icon flex xs:hidden" />
|
<SendIcon className="send-icon flex xs:hidden" />
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const TextForm = ({ step, onSubmit }: TextFormProps) => {
|
|||||||
<SendButton
|
<SendButton
|
||||||
label={step.options?.labels?.button ?? 'Send'}
|
label={step.options?.labels?.button ?? 'Send'}
|
||||||
isDisabled={inputValue === ''}
|
isDisabled={inputValue === ''}
|
||||||
|
className="my-2 ml-2"
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const ConversationContainer = ({
|
|||||||
{displayedBlocks.map((block, idx) => (
|
{displayedBlocks.map((block, idx) => (
|
||||||
<ChatBlock
|
<ChatBlock
|
||||||
key={block.id + idx}
|
key={block.id + idx}
|
||||||
steps={filterTable(block.stepIds, typebot.steps)}
|
stepIds={block.stepIds}
|
||||||
onBlockEnd={displayNextBlock}
|
onBlockEnd={displayNextBlock}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -92,21 +92,23 @@ model Typebot {
|
|||||||
folder DashboardFolder? @relation(fields: [folderId], references: [id])
|
folder DashboardFolder? @relation(fields: [folderId], references: [id])
|
||||||
blocks Json
|
blocks Json
|
||||||
steps Json
|
steps Json
|
||||||
|
choiceItems Json
|
||||||
theme Json
|
theme Json
|
||||||
settings Json
|
settings Json
|
||||||
publicId String? @unique
|
publicId String? @unique
|
||||||
}
|
}
|
||||||
|
|
||||||
model PublicTypebot {
|
model PublicTypebot {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
typebotId String @unique
|
typebotId String @unique
|
||||||
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
|
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
|
||||||
name String
|
name String
|
||||||
blocks Json
|
blocks Json
|
||||||
steps Json
|
steps Json
|
||||||
theme Json
|
choiceItems Json
|
||||||
settings Json
|
theme Json
|
||||||
publicId String? @unique
|
settings Json
|
||||||
|
publicId String? @unique
|
||||||
}
|
}
|
||||||
|
|
||||||
model Result {
|
model Result {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PublicTypebot as PublicTypebotFromPrisma } from 'db'
|
import { PublicTypebot as PublicTypebotFromPrisma } from 'db'
|
||||||
import { Block, Settings, Step, Theme } from './typebot'
|
import { Block, ChoiceItem, Settings, Step, Theme } from './typebot'
|
||||||
import { Table } from './utils'
|
import { Table } from './utils'
|
||||||
|
|
||||||
export type PublicTypebot = Omit<
|
export type PublicTypebot = Omit<
|
||||||
@@ -8,6 +8,7 @@ export type PublicTypebot = Omit<
|
|||||||
> & {
|
> & {
|
||||||
blocks: Table<Block>
|
blocks: Table<Block>
|
||||||
steps: Table<Step>
|
steps: Table<Step>
|
||||||
|
choiceItems: Table<ChoiceItem>
|
||||||
theme: Theme
|
theme: Theme
|
||||||
settings: Settings
|
settings: Settings
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Target } from '.'
|
||||||
import { StepBase } from './steps'
|
import { StepBase } from './steps'
|
||||||
|
|
||||||
export type InputStep =
|
export type InputStep =
|
||||||
@@ -7,6 +8,7 @@ export type InputStep =
|
|||||||
| UrlInputStep
|
| UrlInputStep
|
||||||
| DateInputStep
|
| DateInputStep
|
||||||
| PhoneNumberInputStep
|
| PhoneNumberInputStep
|
||||||
|
| ChoiceInputStep
|
||||||
|
|
||||||
export enum InputStepType {
|
export enum InputStepType {
|
||||||
TEXT = 'text input',
|
TEXT = 'text input',
|
||||||
@@ -15,6 +17,7 @@ export enum InputStepType {
|
|||||||
URL = 'url input',
|
URL = 'url input',
|
||||||
DATE = 'date input',
|
DATE = 'date input',
|
||||||
PHONE = 'phone number input',
|
PHONE = 'phone number input',
|
||||||
|
CHOICE = 'choice input',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TextInputStep = StepBase & {
|
export type TextInputStep = StepBase & {
|
||||||
@@ -44,7 +47,25 @@ export type DateInputStep = StepBase & {
|
|||||||
|
|
||||||
export type PhoneNumberInputStep = StepBase & {
|
export type PhoneNumberInputStep = StepBase & {
|
||||||
type: InputStepType.PHONE
|
type: InputStepType.PHONE
|
||||||
options?: InputOptionsBase
|
options?: InputTextOptionsBase
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChoiceInputStep = StepBase & {
|
||||||
|
type: InputStepType.CHOICE
|
||||||
|
options: ChoiceInputOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChoiceInputOptions = {
|
||||||
|
itemIds: string[]
|
||||||
|
isMultipleChoice?: boolean
|
||||||
|
buttonLabel?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChoiceItem = {
|
||||||
|
id: string
|
||||||
|
stepId: string
|
||||||
|
content?: string
|
||||||
|
target?: Target
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DateInputOptions = {
|
export type DateInputOptions = {
|
||||||
@@ -53,19 +74,19 @@ export type DateInputOptions = {
|
|||||||
isRange?: boolean
|
isRange?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EmailInputOptions = InputOptionsBase
|
export type EmailInputOptions = InputTextOptionsBase
|
||||||
|
|
||||||
export type UrlInputOptions = InputOptionsBase
|
export type UrlInputOptions = InputTextOptionsBase
|
||||||
|
|
||||||
type InputOptionsBase = {
|
type InputTextOptionsBase = {
|
||||||
labels?: { placeholder?: string; button?: string }
|
labels?: { placeholder?: string; button?: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TextInputOptions = InputOptionsBase & {
|
export type TextInputOptions = InputTextOptionsBase & {
|
||||||
isLong?: boolean
|
isLong?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NumberInputOptions = InputOptionsBase & {
|
export type NumberInputOptions = InputTextOptionsBase & {
|
||||||
min?: number
|
min?: number
|
||||||
max?: number
|
max?: number
|
||||||
step?: number
|
step?: number
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Typebot as TypebotFromPrisma } from 'db'
|
import { Typebot as TypebotFromPrisma } from 'db'
|
||||||
|
import { ChoiceItem } from './steps/inputs'
|
||||||
import { Table } from '../utils'
|
import { Table } from '../utils'
|
||||||
import { Settings } from './settings'
|
import { Settings } from './settings'
|
||||||
import { Step } from './steps/steps'
|
import { Step } from './steps/steps'
|
||||||
@@ -10,6 +11,7 @@ export type Typebot = Omit<
|
|||||||
> & {
|
> & {
|
||||||
blocks: Table<Block>
|
blocks: Table<Block>
|
||||||
steps: Table<Step>
|
steps: Table<Step>
|
||||||
|
choiceItems: Table<ChoiceItem>
|
||||||
theme: Theme
|
theme: Theme
|
||||||
settings: Settings
|
settings: Settings
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
BubbleStepType,
|
BubbleStepType,
|
||||||
|
ChoiceInputStep,
|
||||||
InputStep,
|
InputStep,
|
||||||
InputStepType,
|
InputStepType,
|
||||||
Step,
|
Step,
|
||||||
@@ -49,3 +50,9 @@ export const isTextBubbleStep = (step: Step): step is TextStep =>
|
|||||||
|
|
||||||
export const isTextInputStep = (step: Step): step is TextInputStep =>
|
export const isTextInputStep = (step: Step): step is TextInputStep =>
|
||||||
step.type === InputStepType.TEXT
|
step.type === InputStepType.TEXT
|
||||||
|
|
||||||
|
export const isChoiceInput = (step: Step): step is ChoiceInputStep =>
|
||||||
|
step.type === InputStepType.CHOICE
|
||||||
|
|
||||||
|
export const isSingleChoiceInput = (step: Step): step is ChoiceInputStep =>
|
||||||
|
step.type === InputStepType.CHOICE && !step.options.isMultipleChoice
|
||||||
|
|||||||
@@ -6187,6 +6187,11 @@ react-focus-lock@2.5.2:
|
|||||||
use-callback-ref "^1.2.5"
|
use-callback-ref "^1.2.5"
|
||||||
use-sidecar "^1.0.5"
|
use-sidecar "^1.0.5"
|
||||||
|
|
||||||
|
react-frame-component@5.2.2-alpha.0:
|
||||||
|
version "5.2.2-alpha.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-5.2.2-alpha.0.tgz#33d2743ee6f559ac9763490b15e38dbb102c2cb8"
|
||||||
|
integrity sha512-WCbYSTm6JxOrtw4jE30qvHCUv3lFB6DZVNE9npLxNcLxRi3LYbgG0bGOterDk/399MERnYppLr9LmGM0bzmQDw==
|
||||||
|
|
||||||
react-frame-component@^5.2.1:
|
react-frame-component@^5.2.1:
|
||||||
version "5.2.1"
|
version "5.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-5.2.1.tgz#6bd5ec73ef7d720f57ee8f259546ed926a941267"
|
resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-5.2.1.tgz#6bd5ec73ef7d720f57ee8f259546ed926a941267"
|
||||||
|
|||||||
Reference in New Issue
Block a user