chore(editor): ♻️ Revert tables to arrays
Yet another refacto. I improved many many mechanisms on this one including dnd. It is now end 2 end tested 🎉
This commit is contained in:
@ -6,6 +6,7 @@ import {
|
||||
MenuItem,
|
||||
MenuList,
|
||||
} from '@chakra-ui/react'
|
||||
import assert from 'assert'
|
||||
import { DownloadIcon, MoreVerticalIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import React, { useState } from 'react'
|
||||
@ -16,7 +17,7 @@ export const BoardMenuButton = (props: MenuButtonProps) => {
|
||||
const [isDownloading, setIsDownloading] = useState(false)
|
||||
|
||||
const downloadFlow = () => {
|
||||
if (!typebot) return
|
||||
assert(typebot)
|
||||
setIsDownloading(true)
|
||||
const data =
|
||||
'data:application/json;charset=utf-8,' +
|
||||
@ -39,6 +40,7 @@ export const BoardMenuButton = (props: MenuButtonProps) => {
|
||||
colorScheme="blue"
|
||||
icon={<MoreVerticalIcon transform={'rotate(90deg)'} />}
|
||||
isLoading={isDownloading}
|
||||
size="sm"
|
||||
{...props}
|
||||
/>
|
||||
<MenuList>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Flex, HStack, StackProps, Text } from '@chakra-ui/react'
|
||||
import { StepType, DraggableStepType } from 'models'
|
||||
import { useStepDnd } from 'contexts/StepDndContext'
|
||||
import { useStepDnd } from 'contexts/GraphDndContext'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { StepIcon } from './StepIcon'
|
||||
import { StepTypeLabel } from './StepTypeLabel'
|
||||
|
@ -16,7 +16,7 @@ import {
|
||||
IntegrationStepType,
|
||||
LogicStepType,
|
||||
} from 'models'
|
||||
import { useStepDnd } from 'contexts/StepDndContext'
|
||||
import { useStepDnd } from 'contexts/GraphDndContext'
|
||||
import React, { useState } from 'react'
|
||||
import { StepCard, StepCardOverlay } from './StepCard'
|
||||
import { LockedIcon, UnlockedIcon } from 'assets/icons'
|
||||
|
@ -19,7 +19,7 @@ import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
|
||||
export const PreviewDrawer = () => {
|
||||
const { typebot } = useTypebot()
|
||||
const { setRightPanel } = useEditor()
|
||||
const { setPreviewingEdgeId } = useGraph()
|
||||
const { setPreviewingEdge } = useGraph()
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
const [width, setWidth] = useState(500)
|
||||
const [isResizeHandleVisible, setIsResizeHandleVisible] = useState(false)
|
||||
@ -45,10 +45,13 @@ export const PreviewDrawer = () => {
|
||||
}
|
||||
useEventListener('mouseup', handleMouseUp)
|
||||
|
||||
const handleNewBlockVisible = (edgeId: string) => setPreviewingEdgeId(edgeId)
|
||||
|
||||
const handleRestartClick = () => setRestartKey((key) => key + 1)
|
||||
|
||||
const handleCloseClick = () => {
|
||||
setPreviewingEdge(undefined)
|
||||
setRightPanel(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
pos="absolute"
|
||||
@ -75,7 +78,7 @@ export const PreviewDrawer = () => {
|
||||
<VStack w="full" spacing={4}>
|
||||
<Flex justifyContent={'space-between'} w="full">
|
||||
<Button onClick={handleRestartClick}>Restart</Button>
|
||||
<CloseButton onClick={() => setRightPanel(undefined)} />
|
||||
<CloseButton onClick={handleCloseClick} />
|
||||
</Flex>
|
||||
|
||||
{publicTypebot && (
|
||||
@ -89,7 +92,7 @@ export const PreviewDrawer = () => {
|
||||
>
|
||||
<TypebotViewer
|
||||
typebot={publicTypebot}
|
||||
onNewBlockVisible={handleNewBlockVisible}
|
||||
onNewBlockVisible={setPreviewingEdge}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
|
@ -24,7 +24,7 @@ export const SubmissionsTable = ({
|
||||
}: SubmissionsTableProps) => {
|
||||
const { publishedTypebot } = useTypebot()
|
||||
const columns: any = useMemo(
|
||||
() => parseSubmissionsColumns(publishedTypebot),
|
||||
() => (publishedTypebot ? parseSubmissionsColumns(publishedTypebot) : []),
|
||||
[publishedTypebot]
|
||||
)
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Flex, FormLabel, Stack, Switch, Text } from '@chakra-ui/react'
|
||||
import { Flex, FormLabel, Stack, Switch } from '@chakra-ui/react'
|
||||
import { GeneralSettings } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Flex, FormLabel, Stack, Switch, Text } from '@chakra-ui/react'
|
||||
import { Flex, FormLabel, Stack, Switch } from '@chakra-ui/react'
|
||||
import { TypingEmulation } from 'models'
|
||||
import React from 'react'
|
||||
import { isDefined } from 'utils'
|
||||
|
@ -3,7 +3,6 @@ import assert from 'assert'
|
||||
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
|
||||
import { useGraph, ConnectingIds } from 'contexts/GraphContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { Target } from 'models'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import {
|
||||
computeConnectingEdgePath,
|
||||
@ -20,28 +19,26 @@ export const DrawingEdge = () => {
|
||||
targetEndpoints,
|
||||
blocksCoordinates,
|
||||
} = useGraph()
|
||||
const { typebot, createEdge } = useTypebot()
|
||||
const { createEdge } = useTypebot()
|
||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
|
||||
|
||||
const sourceBlock = useMemo(
|
||||
() => connectingIds && typebot?.blocks.byId[connectingIds.source.blockId],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[connectingIds]
|
||||
)
|
||||
const sourceBlockCoordinates =
|
||||
blocksCoordinates && blocksCoordinates[connectingIds?.source.blockId ?? '']
|
||||
const targetBlockCoordinates =
|
||||
blocksCoordinates && blocksCoordinates[connectingIds?.target?.blockId ?? '']
|
||||
|
||||
const sourceTop = useMemo(() => {
|
||||
if (!sourceBlock || !connectingIds) return 0
|
||||
if (!connectingIds) return 0
|
||||
return getEndpointTopOffset(
|
||||
graphPosition,
|
||||
sourceEndpoints,
|
||||
connectingIds.source.buttonId ??
|
||||
connectingIds.source.stepId + (connectingIds.source.conditionType ?? '')
|
||||
connectingIds.source.itemId ?? connectingIds.source.stepId
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [graphPosition, sourceEndpoints, connectingIds])
|
||||
|
||||
const targetTop = useMemo(() => {
|
||||
if (!sourceBlock || !connectingIds) return 0
|
||||
if (!connectingIds) return 0
|
||||
return getEndpointTopOffset(
|
||||
graphPosition,
|
||||
targetEndpoints,
|
||||
@ -51,35 +48,24 @@ export const DrawingEdge = () => {
|
||||
}, [graphPosition, targetEndpoints, connectingIds])
|
||||
|
||||
const path = useMemo(() => {
|
||||
if (
|
||||
!sourceBlock ||
|
||||
!typebot ||
|
||||
!connectingIds ||
|
||||
!blocksCoordinates ||
|
||||
!sourceTop
|
||||
)
|
||||
return ``
|
||||
if (!sourceTop || !sourceBlockCoordinates) return ``
|
||||
|
||||
return connectingIds?.target
|
||||
return targetBlockCoordinates
|
||||
? computeConnectingEdgePath({
|
||||
connectingIds: connectingIds as Omit<ConnectingIds, 'target'> & {
|
||||
target: Target
|
||||
},
|
||||
sourceBlockCoordinates,
|
||||
targetBlockCoordinates,
|
||||
sourceTop,
|
||||
targetTop,
|
||||
blocksCoordinates,
|
||||
})
|
||||
: computeEdgePathToMouse({
|
||||
blockPosition: blocksCoordinates.byId[sourceBlock.id],
|
||||
sourceBlockCoordinates,
|
||||
mousePosition,
|
||||
sourceTop,
|
||||
})
|
||||
}, [
|
||||
sourceBlock,
|
||||
typebot,
|
||||
connectingIds,
|
||||
blocksCoordinates,
|
||||
sourceTop,
|
||||
sourceBlockCoordinates,
|
||||
targetBlockCoordinates,
|
||||
targetTop,
|
||||
mousePosition,
|
||||
])
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Coordinates, useGraph } from 'contexts/GraphContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import React, { useMemo } from 'react'
|
||||
import {
|
||||
getAnchorsPosition,
|
||||
@ -7,6 +6,7 @@ import {
|
||||
getEndpointTopOffset,
|
||||
getSourceEndpointId,
|
||||
} from 'services/graph'
|
||||
import { Edge as EdgeProps } from 'models'
|
||||
|
||||
export type AnchorsPositionProps = {
|
||||
sourcePosition: Coordinates
|
||||
@ -15,28 +15,20 @@ export type AnchorsPositionProps = {
|
||||
totalSegments: number
|
||||
}
|
||||
|
||||
export const Edge = ({ edgeId }: { edgeId: string }) => {
|
||||
const { typebot } = useTypebot()
|
||||
export const Edge = ({ edge }: { edge: EdgeProps }) => {
|
||||
const {
|
||||
previewingEdgeId,
|
||||
previewingEdge,
|
||||
sourceEndpoints,
|
||||
targetEndpoints,
|
||||
graphPosition,
|
||||
blocksCoordinates,
|
||||
} = useGraph()
|
||||
const edge = useMemo(
|
||||
() => typebot?.edges.byId[edgeId],
|
||||
[edgeId, typebot?.edges.byId]
|
||||
)
|
||||
const isPreviewing = previewingEdgeId === edgeId
|
||||
|
||||
const sourceBlock = edge && typebot?.blocks.byId[edge.from.blockId]
|
||||
const targetBlock = edge && typebot?.blocks.byId[edge.to.blockId]
|
||||
const isPreviewing = previewingEdge?.id === edge.id
|
||||
|
||||
const sourceBlockCoordinates =
|
||||
sourceBlock && blocksCoordinates?.byId[sourceBlock.id]
|
||||
blocksCoordinates && blocksCoordinates[edge.from.blockId]
|
||||
const targetBlockCoordinates =
|
||||
targetBlock && blocksCoordinates?.byId[targetBlock.id]
|
||||
blocksCoordinates && blocksCoordinates[edge.to.blockId]
|
||||
|
||||
const sourceTop = useMemo(
|
||||
() =>
|
||||
@ -77,6 +69,7 @@ export const Edge = ({ edgeId }: { edgeId: string }) => {
|
||||
if (sourceTop === 0) return <></>
|
||||
return (
|
||||
<path
|
||||
data-testid="edge"
|
||||
d={path}
|
||||
stroke={isPreviewing ? '#1a5fff' : '#718096'}
|
||||
strokeWidth="2px"
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { chakra } from '@chakra-ui/system'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { Edge as EdgeProps } from 'models'
|
||||
import React from 'react'
|
||||
import { DrawingEdge } from './DrawingEdge'
|
||||
import { Edge } from './Edge'
|
||||
|
||||
export const Edges = () => {
|
||||
const { typebot } = useTypebot()
|
||||
|
||||
type Props = {
|
||||
edges: EdgeProps[]
|
||||
}
|
||||
export const Edges = ({ edges }: Props) => {
|
||||
return (
|
||||
<chakra.svg
|
||||
width="full"
|
||||
@ -17,8 +18,8 @@ export const Edges = () => {
|
||||
top="0"
|
||||
>
|
||||
<DrawingEdge />
|
||||
{typebot?.edges.allIds.map((edgeId) => (
|
||||
<Edge key={edgeId} edgeId={edgeId} />
|
||||
{edges.map((edge) => (
|
||||
<Edge key={edge.id} edge={edge} />
|
||||
))}
|
||||
<marker
|
||||
id={'arrow'}
|
||||
|
@ -19,29 +19,32 @@ export const SourceEndpoint = ({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (ranOnce || !ref.current) return
|
||||
const id = source.buttonId ?? source.stepId + (source.conditionType ?? '')
|
||||
if (ranOnce || !ref.current || Object.keys(blocksCoordinates).length === 0)
|
||||
return
|
||||
const id = source.itemId ?? source.stepId
|
||||
addSourceEndpoint({
|
||||
id,
|
||||
ref,
|
||||
})
|
||||
setRanOnce(true)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ref.current])
|
||||
}, [ref.current, blocksCoordinates])
|
||||
|
||||
if (!blocksCoordinates) return <></>
|
||||
return (
|
||||
<Flex
|
||||
ref={ref}
|
||||
data-testid="endpoint"
|
||||
boxSize="18px"
|
||||
rounded="full"
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseDownCapture={handleMouseDown}
|
||||
cursor="copy"
|
||||
borderWidth="1px"
|
||||
borderColor="gray.400"
|
||||
bgColor="white"
|
||||
justify="center"
|
||||
align="center"
|
||||
pointerEvents="all"
|
||||
{...props}
|
||||
>
|
||||
<Box bgColor="gray.400" rounded="full" boxSize="6px" />
|
||||
|
@ -2,7 +2,7 @@ import { Flex, FlexProps, useEventListener } from '@chakra-ui/react'
|
||||
import React, { useRef, useMemo, useEffect } from 'react'
|
||||
import { blockWidth, useGraph } from 'contexts/GraphContext'
|
||||
import { BlockNode } from './Nodes/BlockNode/BlockNode'
|
||||
import { useStepDnd } from 'contexts/StepDndContext'
|
||||
import { useStepDnd } from 'contexts/GraphDndContext'
|
||||
import { Edges } from './Edges'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { headerHeight } from 'components/shared/TypebotHeader/TypebotHeader'
|
||||
@ -58,6 +58,7 @@ export const Graph = ({
|
||||
useEventListener('wheel', handleMouseWheel, graphContainerRef.current)
|
||||
|
||||
const handleMouseUp = (e: MouseEvent) => {
|
||||
if (!typebot) return
|
||||
if (!draggedStep && !draggedStepType) return
|
||||
const coordinates = {
|
||||
x: e.clientX - graphPosition.x - blockWidth / 3,
|
||||
@ -69,6 +70,7 @@ export const Graph = ({
|
||||
id,
|
||||
...coordinates,
|
||||
step: draggedStep ?? (draggedStepType as DraggableStepType),
|
||||
indices: { blockIndex: typebot.blocks.length, stepIndex: 0 },
|
||||
})
|
||||
setDraggedStep(undefined)
|
||||
setDraggedStepType(undefined)
|
||||
@ -84,7 +86,6 @@ export const Graph = ({
|
||||
const handleClick = () => setOpenedStepId(undefined)
|
||||
useEventListener('click', handleClick, editorContainerRef.current)
|
||||
|
||||
if (!typebot) return <></>
|
||||
return (
|
||||
<Flex ref={graphContainerRef} {...props}>
|
||||
<Flex
|
||||
@ -95,9 +96,9 @@ export const Graph = ({
|
||||
transform,
|
||||
}}
|
||||
>
|
||||
<Edges />
|
||||
{typebot.blocks.allIds.map((blockId) => (
|
||||
<BlockNode block={typebot.blocks.byId[blockId]} key={blockId} />
|
||||
<Edges edges={typebot?.edges ?? []} />
|
||||
{typebot?.blocks.map((block, idx) => (
|
||||
<BlockNode block={block} blockIndex={idx} key={block.id} />
|
||||
))}
|
||||
{answersCounts?.map((answersCount) => (
|
||||
<DropOffNode
|
||||
|
@ -5,45 +5,44 @@ import {
|
||||
Stack,
|
||||
useEventListener,
|
||||
} from '@chakra-ui/react'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { Block } from 'models'
|
||||
import { useGraph } from 'contexts/GraphContext'
|
||||
import { useStepDnd } from 'contexts/StepDndContext'
|
||||
import { useStepDnd } from 'contexts/GraphDndContext'
|
||||
import { StepNodesList } from '../StepNode/StepNodesList'
|
||||
import { isNotDefined } from 'utils'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { ContextMenu } from 'components/shared/ContextMenu'
|
||||
import { BlockNodeContextMenu } from './BlockNodeContextMenu'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { setMultipleRefs } from 'services/utils'
|
||||
|
||||
type Props = {
|
||||
block: Block
|
||||
blockIndex: number
|
||||
}
|
||||
|
||||
export const BlockNode = ({ block }: Props) => {
|
||||
export const BlockNode = ({ block, blockIndex }: Props) => {
|
||||
const {
|
||||
connectingIds,
|
||||
setConnectingIds,
|
||||
previewingEdgeId,
|
||||
previewingEdge,
|
||||
blocksCoordinates,
|
||||
updateBlockCoordinates,
|
||||
isReadOnly,
|
||||
} = useGraph()
|
||||
const { typebot, updateBlock } = useTypebot()
|
||||
const { setMouseOverBlockId } = useStepDnd()
|
||||
const { draggedStep, draggedStepType } = useStepDnd()
|
||||
const { setMouseOverBlock, mouseOverBlock } = useStepDnd()
|
||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||
const [isConnecting, setIsConnecting] = useState(false)
|
||||
const isPreviewing = useMemo(() => {
|
||||
if (!previewingEdgeId) return
|
||||
const edge = typebot?.edges.byId[previewingEdgeId]
|
||||
return edge?.to.blockId === block.id || edge?.from.blockId === block.id
|
||||
}, [block.id, previewingEdgeId, typebot?.edges.byId])
|
||||
const isPreviewing =
|
||||
previewingEdge?.to.blockId === block.id ||
|
||||
previewingEdge?.from.blockId === block.id
|
||||
const isStartBlock =
|
||||
block.steps.length === 1 && block.steps[0].type === 'start'
|
||||
|
||||
const blockCoordinates = useMemo(
|
||||
() => blocksCoordinates?.byId[block.id],
|
||||
[block.id, blocksCoordinates?.byId]
|
||||
)
|
||||
const blockCoordinates = blocksCoordinates[block.id]
|
||||
const blockRef = useRef<HTMLDivElement | null>(null)
|
||||
const [debouncedBlockPosition] = useDebounce(blockCoordinates, 100)
|
||||
useEffect(() => {
|
||||
if (!debouncedBlockPosition || isReadOnly) return
|
||||
@ -52,7 +51,7 @@ export const BlockNode = ({ block }: Props) => {
|
||||
debouncedBlockPosition.y === block.graphCoordinates.y
|
||||
)
|
||||
return
|
||||
updateBlock(block.id, { graphCoordinates: debouncedBlockPosition })
|
||||
updateBlock(blockIndex, { graphCoordinates: debouncedBlockPosition })
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedBlockPosition])
|
||||
|
||||
@ -63,7 +62,8 @@ export const BlockNode = ({ block }: Props) => {
|
||||
)
|
||||
}, [block.id, connectingIds])
|
||||
|
||||
const handleTitleSubmit = (title: string) => updateBlock(block.id, { title })
|
||||
const handleTitleSubmit = (title: string) =>
|
||||
updateBlock(blockIndex, { title })
|
||||
|
||||
const handleMouseDown = () => {
|
||||
setIsMouseDown(true)
|
||||
@ -87,24 +87,26 @@ export const BlockNode = ({ block }: Props) => {
|
||||
useEventListener('mousemove', handleMouseMove)
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (draggedStepType || draggedStep) setMouseOverBlockId(block.id)
|
||||
if (mouseOverBlock?.id !== block.id && !isStartBlock)
|
||||
setMouseOverBlock({ id: block.id, ref: blockRef })
|
||||
if (connectingIds)
|
||||
setConnectingIds({ ...connectingIds, target: { blockId: block.id } })
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setMouseOverBlockId(undefined)
|
||||
setMouseOverBlock(undefined)
|
||||
if (connectingIds) setConnectingIds({ ...connectingIds, target: undefined })
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenu<HTMLDivElement>
|
||||
renderMenu={() => <BlockNodeContextMenu blockId={block.id} />}
|
||||
renderMenu={() => <BlockNodeContextMenu blockIndex={blockIndex} />}
|
||||
isDisabled={isReadOnly}
|
||||
>
|
||||
{(ref, isOpened) => (
|
||||
<Stack
|
||||
ref={ref}
|
||||
ref={setMultipleRefs([ref, blockRef])}
|
||||
data-testid="block"
|
||||
p="4"
|
||||
rounded="lg"
|
||||
bgColor="blue.50"
|
||||
@ -142,7 +144,13 @@ export const BlockNode = ({ block }: Props) => {
|
||||
<EditableInput minW="0" px="1" />
|
||||
</Editable>
|
||||
{typebot && (
|
||||
<StepNodesList blockId={block.id} stepIds={block.stepIds} />
|
||||
<StepNodesList
|
||||
blockId={block.id}
|
||||
steps={block.steps}
|
||||
blockIndex={blockIndex}
|
||||
blockRef={ref}
|
||||
isStartBlock={isStartBlock}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
|
@ -2,10 +2,14 @@ import { MenuList, MenuItem } from '@chakra-ui/react'
|
||||
import { TrashIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
|
||||
export const BlockNodeContextMenu = ({ blockId }: { blockId: string }) => {
|
||||
export const BlockNodeContextMenu = ({
|
||||
blockIndex,
|
||||
}: {
|
||||
blockIndex: number
|
||||
}) => {
|
||||
const { deleteBlock } = useTypebot()
|
||||
|
||||
const handleDeleteClick = () => deleteBlock(blockId)
|
||||
const handleDeleteClick = () => deleteBlock(blockIndex)
|
||||
|
||||
return (
|
||||
<MenuList>
|
||||
|
@ -1,156 +0,0 @@
|
||||
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 { isNotDefined, isSingleChoiceInput } from 'utils'
|
||||
import { SourceEndpoint } from '../../Endpoints/SourceEndpoint'
|
||||
import { ButtonNodeContextMenu } from './ButtonNodeContextMenu'
|
||||
|
||||
type Props = {
|
||||
item: ChoiceItem
|
||||
onMouseMoveBottomOfElement?: () => void
|
||||
onMouseMoveTopOfElement?: () => void
|
||||
onMouseDown?: (
|
||||
stepNodePosition: { absolute: Coordinates; relative: Coordinates },
|
||||
item: ChoiceItem
|
||||
) => void
|
||||
}
|
||||
|
||||
export const ButtonNode = ({
|
||||
item,
|
||||
onMouseDown,
|
||||
onMouseMoveBottomOfElement,
|
||||
onMouseMoveTopOfElement,
|
||||
}: Props) => {
|
||||
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={() => <ButtonNodeContextMenu itemId={item.id} />}
|
||||
>
|
||||
{(ref, isOpened) => (
|
||||
<Flex
|
||||
ref={ref}
|
||||
align="center"
|
||||
pos="relative"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
justifyContent="center"
|
||||
shadow="sm"
|
||||
_hover={{ shadow: 'md' }}
|
||||
transition="box-shadow 200ms"
|
||||
borderWidth="1px"
|
||||
rounded="md"
|
||||
px="4"
|
||||
py="2"
|
||||
borderColor={isOpened ? 'blue.400' : 'gray.300'}
|
||||
>
|
||||
<Editable
|
||||
defaultValue={item.content ?? 'Click to edit'}
|
||||
flex="1"
|
||||
startWithEditView={isNotDefined(item.content)}
|
||||
onSubmit={handleInputSubmit}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
>
|
||||
<EditablePreview
|
||||
w="full"
|
||||
color={item.content !== 'Click to edit' ? 'inherit' : 'gray.500'}
|
||||
/>
|
||||
<EditableInput />
|
||||
</Editable>
|
||||
{typebot && isSingleChoiceInput(typebot.steps.byId[item.stepId]) && (
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
blockId: typebot.steps.byId[item.stepId].blockId,
|
||||
stepId: item.stepId,
|
||||
buttonId: 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"
|
||||
shadow="md"
|
||||
colorScheme="blue"
|
||||
onClick={handlePlusClick}
|
||||
/>
|
||||
</Fade>
|
||||
</Flex>
|
||||
)}
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
@ -1,154 +0,0 @@
|
||||
import { Flex, Portal, Stack, Text, useEventListener } from '@chakra-ui/react'
|
||||
import { useStepDnd } from 'contexts/StepDndContext'
|
||||
import { Coordinates } from 'contexts/GraphContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { ChoiceInputStep, ChoiceItem } from 'models'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { ButtonNode } from './ButtonNode'
|
||||
import { SourceEndpoint } from '../../Endpoints'
|
||||
import { ButtonNodeOverlay } from './ButtonNodeOverlay'
|
||||
|
||||
type ChoiceItemsListProps = {
|
||||
step: ChoiceInputStep
|
||||
}
|
||||
|
||||
export const ButtonNodesList = ({ step }: ChoiceItemsListProps) => {
|
||||
const { typebot, createChoiceItem } = useTypebot()
|
||||
const {
|
||||
draggedChoiceItem,
|
||||
mouseOverBlockId,
|
||||
setDraggedChoiceItem,
|
||||
setMouseOverBlockId,
|
||||
} = useStepDnd()
|
||||
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: MouseEvent) => {
|
||||
if (!draggedChoiceItem) return
|
||||
if (expandedPlaceholderIndex !== -1) {
|
||||
e.stopPropagation()
|
||||
createChoiceItem(draggedChoiceItem, expandedPlaceholderIndex)
|
||||
}
|
||||
setMouseOverBlockId(undefined)
|
||||
setExpandedPlaceholderIndex(undefined)
|
||||
setDraggedChoiceItem(undefined)
|
||||
}
|
||||
useEventListener('mouseup', handleMouseUp, undefined, { capture: true })
|
||||
|
||||
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} onClick={stopPropagating}>
|
||||
<Flex
|
||||
h={expandedPlaceholderIndex === 0 ? '50px' : '2px'}
|
||||
bgColor={'gray.300'}
|
||||
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] && (
|
||||
<ButtonNode
|
||||
item={typebot?.choiceItems.byId[itemId]}
|
||||
onMouseMoveTopOfElement={() => handleMouseOnTopOfStep(idx)}
|
||||
onMouseMoveBottomOfElement={() => {
|
||||
handleMouseOnBottomOfStep(idx)
|
||||
}}
|
||||
onMouseDown={handleStepMouseDown}
|
||||
/>
|
||||
)}
|
||||
<Flex
|
||||
h={
|
||||
showSortPlaceholders && expandedPlaceholderIndex === idx + 1
|
||||
? '50px'
|
||||
: '2px'
|
||||
}
|
||||
bgColor={'gray.300'}
|
||||
visibility={showSortPlaceholders ? 'visible' : 'hidden'}
|
||||
rounded="lg"
|
||||
transition={showSortPlaceholders ? 'height 200ms' : 'none'}
|
||||
/>
|
||||
</Stack>
|
||||
))}
|
||||
<Stack>
|
||||
<Flex
|
||||
px="4"
|
||||
py="2"
|
||||
borderWidth="1px"
|
||||
borderColor="gray.300"
|
||||
bgColor="gray.50"
|
||||
rounded="md"
|
||||
pos="relative"
|
||||
align="center"
|
||||
cursor="not-allowed"
|
||||
>
|
||||
<Text color="gray.500">Default</Text>
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
blockId: step.blockId,
|
||||
stepId: step.id,
|
||||
}}
|
||||
pos="absolute"
|
||||
right="15px"
|
||||
/>
|
||||
</Flex>
|
||||
</Stack>
|
||||
|
||||
{draggedChoiceItem && draggedChoiceItem.stepId === step.id && (
|
||||
<Portal>
|
||||
<ButtonNodeOverlay
|
||||
item={draggedChoiceItem}
|
||||
pos="fixed"
|
||||
top="0"
|
||||
left="0"
|
||||
style={{
|
||||
transform: `translate(${position.x}px, ${position.y}px) rotate(-2deg)`,
|
||||
}}
|
||||
/>
|
||||
</Portal>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -1 +0,0 @@
|
||||
export { ButtonNodesList } from './ButtonNodesList'
|
@ -3,7 +3,7 @@ import { useTypebot } from 'contexts/TypebotContext'
|
||||
import React, { useMemo } from 'react'
|
||||
import { AnswersCount } from 'services/analytics'
|
||||
import { computeSourceCoordinates } from 'services/graph'
|
||||
import { isDefined } from 'utils'
|
||||
import { byId, isDefined } from 'utils'
|
||||
|
||||
type Props = {
|
||||
answersCounts: AnswersCount[]
|
||||
@ -21,11 +21,10 @@ export const DropOffNode = ({ answersCounts, blockId }: Props) => {
|
||||
const { totalDroppedUser, dropOffRate } = useMemo(() => {
|
||||
if (!typebot || totalAnswers === undefined)
|
||||
return { previousTotal: undefined, dropOffRate: undefined }
|
||||
const previousBlockIds = typebot.edges.allIds
|
||||
.map((edgeId) => {
|
||||
const edge = typebot.edges.byId[edgeId]
|
||||
return edge.to.blockId === blockId ? edge.from.blockId : undefined
|
||||
})
|
||||
const previousBlockIds = typebot.edges
|
||||
.map((edge) =>
|
||||
edge.to.blockId === blockId ? edge.from.blockId : undefined
|
||||
)
|
||||
.filter((blockId) => isDefined(blockId))
|
||||
const previousTotal = answersCounts
|
||||
.filter((a) => previousBlockIds.includes(a.blockId))
|
||||
@ -41,12 +40,11 @@ export const DropOffNode = ({ answersCounts, blockId }: Props) => {
|
||||
}, [answersCounts, blockId, totalAnswers, typebot])
|
||||
|
||||
const labelCoordinates = useMemo(() => {
|
||||
if (!typebot) return { x: 0, y: 0 }
|
||||
const sourceBlock = typebot?.blocks.byId[blockId]
|
||||
const sourceBlock = typebot?.blocks.find(byId(blockId))
|
||||
if (!sourceBlock) return
|
||||
return computeSourceCoordinates(
|
||||
sourceBlock?.graphCoordinates,
|
||||
sourceBlock?.stepIds.length - 1
|
||||
sourceBlock?.steps.length - 1
|
||||
)
|
||||
}, [blockId, typebot])
|
||||
|
||||
|
@ -0,0 +1,90 @@
|
||||
import { Flex } from '@chakra-ui/react'
|
||||
import { ContextMenu } from 'components/shared/ContextMenu'
|
||||
import { Coordinates } from 'contexts/GraphContext'
|
||||
import { NodePosition, useDragDistance } from 'contexts/GraphDndContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { ButtonItem, Item, ItemIndices, ItemType } from 'models'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { setMultipleRefs } from 'services/utils'
|
||||
import { SourceEndpoint } from '../../Endpoints/SourceEndpoint'
|
||||
import { ItemNodeContent } from './ItemNodeContent'
|
||||
import { ItemNodeContextMenu } from './ItemNodeContextMenu'
|
||||
|
||||
type Props = {
|
||||
item: Item
|
||||
indices: ItemIndices
|
||||
isReadOnly: boolean
|
||||
isLastItem: boolean
|
||||
onMouseDown?: (
|
||||
stepNodePosition: { absolute: Coordinates; relative: Coordinates },
|
||||
item: ButtonItem
|
||||
) => void
|
||||
}
|
||||
|
||||
export const ItemNode = ({
|
||||
item,
|
||||
indices,
|
||||
isReadOnly,
|
||||
isLastItem,
|
||||
onMouseDown,
|
||||
}: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
const [isMouseOver, setIsMouseOver] = useState(false)
|
||||
const itemRef = useRef<HTMLDivElement | null>(null)
|
||||
const onDrag = (position: NodePosition) => {
|
||||
if (!onMouseDown || item.type !== ItemType.BUTTON) return
|
||||
onMouseDown(position, item)
|
||||
}
|
||||
useDragDistance({
|
||||
ref: itemRef,
|
||||
onDrag,
|
||||
isDisabled: !onMouseDown || item.type !== ItemType.BUTTON,
|
||||
})
|
||||
|
||||
const handleMouseEnter = () => setIsMouseOver(true)
|
||||
const handleMouseLeave = () => setIsMouseOver(false)
|
||||
|
||||
return (
|
||||
<ContextMenu<HTMLDivElement>
|
||||
renderMenu={() => <ItemNodeContextMenu indices={indices} />}
|
||||
>
|
||||
{(ref, isOpened) => (
|
||||
<Flex
|
||||
data-testid="item"
|
||||
ref={setMultipleRefs([ref, itemRef])}
|
||||
align="center"
|
||||
pos="relative"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
shadow="sm"
|
||||
_hover={isReadOnly ? {} : { shadow: 'md' }}
|
||||
transition="box-shadow 200ms"
|
||||
borderWidth="1px"
|
||||
rounded="md"
|
||||
borderColor={isOpened ? 'blue.400' : 'gray.300'}
|
||||
pointerEvents={isReadOnly ? 'none' : 'all'}
|
||||
w="full"
|
||||
>
|
||||
<ItemNodeContent
|
||||
item={item}
|
||||
isMouseOver={isMouseOver}
|
||||
indices={indices}
|
||||
isLastItem={isLastItem}
|
||||
/>
|
||||
{typebot && (
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
blockId: typebot.blocks[indices.blockIndex].id,
|
||||
stepId: item.stepId,
|
||||
itemId: item.id,
|
||||
}}
|
||||
pos="absolute"
|
||||
right="15px"
|
||||
pointerEvents="all"
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import { Item, ItemIndices, ItemType } from 'models'
|
||||
import React from 'react'
|
||||
import { ButtonNodeContent } from './contents/ButtonNodeContent'
|
||||
import { ConditionNodeContent } from './contents/ConditionNodeContent'
|
||||
|
||||
type Props = {
|
||||
item: Item
|
||||
indices: ItemIndices
|
||||
isMouseOver: boolean
|
||||
isLastItem: boolean
|
||||
}
|
||||
|
||||
export const ItemNodeContent = ({
|
||||
item,
|
||||
indices,
|
||||
isMouseOver,
|
||||
isLastItem,
|
||||
}: Props) => {
|
||||
switch (item.type) {
|
||||
case ItemType.BUTTON:
|
||||
return (
|
||||
<ButtonNodeContent
|
||||
item={item}
|
||||
isMouseOver={isMouseOver}
|
||||
indices={indices}
|
||||
isLastItem={isLastItem}
|
||||
/>
|
||||
)
|
||||
case ItemType.CONDITION:
|
||||
return <ConditionNodeContent item={item} />
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
import {
|
||||
EditablePreview,
|
||||
EditableInput,
|
||||
Editable,
|
||||
Fade,
|
||||
IconButton,
|
||||
Flex,
|
||||
} from '@chakra-ui/react'
|
||||
import { PlusIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { ButtonItem, ItemIndices, ItemType } from 'models'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { isNotDefined } from 'utils'
|
||||
|
||||
type Props = {
|
||||
item: ButtonItem
|
||||
indices: ItemIndices
|
||||
isLastItem: boolean
|
||||
isMouseOver: boolean
|
||||
}
|
||||
|
||||
export const ButtonNodeContent = ({
|
||||
item,
|
||||
indices,
|
||||
isMouseOver,
|
||||
isLastItem,
|
||||
}: Props) => {
|
||||
const { deleteItem, updateItem, createItem } = useTypebot()
|
||||
const [initialContent] = useState(item.content ?? '')
|
||||
const [itemValue, setItemValue] = useState(item.content ?? 'Click to edit')
|
||||
const editableRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const handleInputSubmit = () => {
|
||||
if (itemValue === '') deleteItem(indices)
|
||||
else
|
||||
updateItem(indices, { content: itemValue === '' ? undefined : itemValue })
|
||||
}
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === 'Escape' && itemValue === 'Click to edit') deleteItem(indices)
|
||||
if (
|
||||
e.key === 'Enter' &&
|
||||
itemValue !== '' &&
|
||||
isLastItem &&
|
||||
initialContent === ''
|
||||
)
|
||||
handlePlusClick()
|
||||
}
|
||||
|
||||
const handlePlusClick = () => {
|
||||
const itemIndex = indices.itemIndex + 1
|
||||
createItem(
|
||||
{ stepId: item.stepId, type: ItemType.BUTTON },
|
||||
{ ...indices, itemIndex }
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex px={4} py={2} justify="center" w="full">
|
||||
<Editable
|
||||
ref={editableRef}
|
||||
flex="1"
|
||||
startWithEditView={isNotDefined(item.content)}
|
||||
value={itemValue}
|
||||
onChange={setItemValue}
|
||||
onSubmit={handleInputSubmit}
|
||||
onKeyDownCapture={handleKeyPress}
|
||||
>
|
||||
<EditablePreview
|
||||
w="full"
|
||||
color={item.content !== 'Click to edit' ? 'inherit' : 'gray.500'}
|
||||
cursor="pointer"
|
||||
/>
|
||||
<EditableInput />
|
||||
</Editable>
|
||||
<Fade
|
||||
in={isMouseOver}
|
||||
style={{ position: 'absolute', bottom: '-15px', zIndex: 3 }}
|
||||
unmountOnExit
|
||||
>
|
||||
<IconButton
|
||||
aria-label="Add item"
|
||||
icon={<PlusIcon />}
|
||||
size="xs"
|
||||
shadow="md"
|
||||
colorScheme="blue"
|
||||
onClick={handlePlusClick}
|
||||
/>
|
||||
</Fade>
|
||||
</Flex>
|
||||
)
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
import { Stack, Tag, Text, Flex, Wrap } from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { Comparison, ConditionItem, ComparisonOperators } from 'models'
|
||||
import React from 'react'
|
||||
import { byId, isNotDefined } from 'utils'
|
||||
|
||||
type Props = {
|
||||
item: ConditionItem
|
||||
}
|
||||
|
||||
export const ConditionNodeContent = ({ item }: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
return (
|
||||
<Flex px={2} py={2}>
|
||||
{item.content.comparisons.length === 0 ||
|
||||
comparisonIsEmpty(item.content.comparisons[0]) ? (
|
||||
<Text color={'gray.500'}>Configure...</Text>
|
||||
) : (
|
||||
<Stack maxW="170px">
|
||||
{item.content.comparisons.map((comparison, idx) => {
|
||||
const variable = typebot?.variables.find(
|
||||
byId(comparison.variableId)
|
||||
)
|
||||
return (
|
||||
<Wrap key={comparison.id} spacing={1} isTruncated>
|
||||
{idx > 0 && <Text>{item.content.logicalOperator ?? ''}</Text>}
|
||||
{variable?.name && (
|
||||
<Tag bgColor="orange.400" color="white">
|
||||
{variable.name}
|
||||
</Tag>
|
||||
)}
|
||||
{comparison.comparisonOperator && (
|
||||
<Text>
|
||||
{parseComparisonOperatorSymbol(
|
||||
comparison.comparisonOperator
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
{comparison?.value && (
|
||||
<Tag bgColor={'gray.200'}>
|
||||
<Text isTruncated>{comparison.value}</Text>
|
||||
</Tag>
|
||||
)}
|
||||
</Wrap>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
const comparisonIsEmpty = (comparison: Comparison) =>
|
||||
isNotDefined(comparison.comparisonOperator) &&
|
||||
isNotDefined(comparison.value) &&
|
||||
isNotDefined(comparison.variableId)
|
||||
|
||||
const parseComparisonOperatorSymbol = (operator: ComparisonOperators) => {
|
||||
switch (operator) {
|
||||
case ComparisonOperators.CONTAINS:
|
||||
return 'contains'
|
||||
case ComparisonOperators.EQUAL:
|
||||
return '='
|
||||
case ComparisonOperators.GREATER:
|
||||
return '>'
|
||||
case ComparisonOperators.IS_SET:
|
||||
return 'is set'
|
||||
case ComparisonOperators.LESS:
|
||||
return '<'
|
||||
case ComparisonOperators.NOT_EQUAL:
|
||||
return '!='
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { ItemNodeContent } from './ItemNodeContent'
|
@ -1,11 +1,15 @@
|
||||
import { MenuList, MenuItem } from '@chakra-ui/react'
|
||||
import { TrashIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { ItemIndices } from 'models'
|
||||
|
||||
export const ButtonNodeContextMenu = ({ itemId }: { itemId: string }) => {
|
||||
const { deleteChoiceItem } = useTypebot()
|
||||
type Props = {
|
||||
indices: ItemIndices
|
||||
}
|
||||
export const ItemNodeContextMenu = ({ indices }: Props) => {
|
||||
const { deleteItem } = useTypebot()
|
||||
|
||||
const handleDeleteClick = () => deleteChoiceItem(itemId)
|
||||
const handleDeleteClick = () => deleteItem(indices)
|
||||
|
||||
return (
|
||||
<MenuList>
|
@ -1,12 +1,12 @@
|
||||
import { Flex, FlexProps } from '@chakra-ui/react'
|
||||
import { ChoiceItem } from 'models'
|
||||
import { Item } from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
item: ChoiceItem
|
||||
item: Item
|
||||
} & FlexProps
|
||||
|
||||
export const ButtonNodeOverlay = ({ item, ...props }: Props) => {
|
||||
export const ItemNodeOverlay = ({ item, ...props }: Props) => {
|
||||
return (
|
||||
<Flex
|
||||
px="4"
|
@ -0,0 +1,189 @@
|
||||
import { Flex, Portal, Stack, Text, useEventListener } from '@chakra-ui/react'
|
||||
import {
|
||||
computeNearestPlaceholderIndex,
|
||||
useStepDnd,
|
||||
} from 'contexts/GraphDndContext'
|
||||
import { Coordinates } from 'contexts/GraphContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { ButtonItem, StepIndices, StepWithItems } from 'models'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { ItemNode } from './ItemNode'
|
||||
import { SourceEndpoint } from '../../Endpoints'
|
||||
import { ItemNodeOverlay } from './ItemNodeOverlay'
|
||||
|
||||
type Props = {
|
||||
step: StepWithItems
|
||||
indices: StepIndices
|
||||
isReadOnly?: boolean
|
||||
}
|
||||
|
||||
export const ItemNodesList = ({
|
||||
step,
|
||||
indices: { blockIndex, stepIndex },
|
||||
isReadOnly = false,
|
||||
}: Props) => {
|
||||
const { typebot, createItem, deleteItem } = useTypebot()
|
||||
const { draggedItem, setDraggedItem, mouseOverBlock } = useStepDnd()
|
||||
const placeholderRefs = useRef<HTMLDivElement[]>([])
|
||||
const blockId = typebot?.blocks[blockIndex].id
|
||||
const isDraggingOnCurrentBlock =
|
||||
(draggedItem && mouseOverBlock?.id === blockId) ?? false
|
||||
const showPlaceholders = draggedItem && !isReadOnly
|
||||
|
||||
const [position, setPosition] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
const [relativeCoordinates, setRelativeCoordinates] = useState({ x: 0, y: 0 })
|
||||
const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState<
|
||||
number | undefined
|
||||
>()
|
||||
|
||||
const handleGlobalMouseMove = (event: MouseEvent) => {
|
||||
if (!draggedItem || draggedItem.stepId !== step.id) return
|
||||
const { clientX, clientY } = event
|
||||
setPosition({
|
||||
...position,
|
||||
x: clientX - relativeCoordinates.x,
|
||||
y: clientY - relativeCoordinates.y,
|
||||
})
|
||||
}
|
||||
useEventListener('mousemove', handleGlobalMouseMove)
|
||||
|
||||
useEffect(() => {
|
||||
if (mouseOverBlock?.id !== step.blockId)
|
||||
setExpandedPlaceholderIndex(undefined)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mouseOverBlock?.id])
|
||||
|
||||
const handleMouseMoveOnBlock = (event: MouseEvent) => {
|
||||
if (!isDraggingOnCurrentBlock || isReadOnly) return
|
||||
const index = computeNearestPlaceholderIndex(event.pageY, placeholderRefs)
|
||||
setExpandedPlaceholderIndex(index)
|
||||
}
|
||||
useEventListener(
|
||||
'mousemove',
|
||||
handleMouseMoveOnBlock,
|
||||
mouseOverBlock?.ref.current
|
||||
)
|
||||
|
||||
const handleMouseUpOnBlock = (e: MouseEvent) => {
|
||||
setExpandedPlaceholderIndex(undefined)
|
||||
if (!isDraggingOnCurrentBlock) return
|
||||
const itemIndex = computeNearestPlaceholderIndex(e.pageY, placeholderRefs)
|
||||
e.stopPropagation()
|
||||
createItem(draggedItem as ButtonItem, {
|
||||
blockIndex,
|
||||
stepIndex,
|
||||
itemIndex,
|
||||
})
|
||||
setDraggedItem(undefined)
|
||||
}
|
||||
useEventListener(
|
||||
'mouseup',
|
||||
handleMouseUpOnBlock,
|
||||
mouseOverBlock?.ref.current,
|
||||
{
|
||||
capture: true,
|
||||
}
|
||||
)
|
||||
|
||||
const handleStepMouseDown =
|
||||
(itemIndex: number) =>
|
||||
(
|
||||
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
|
||||
item: ButtonItem
|
||||
) => {
|
||||
if (!typebot || isReadOnly) return
|
||||
deleteItem({ blockIndex, stepIndex, itemIndex })
|
||||
setPosition(absolute)
|
||||
setRelativeCoordinates(relative)
|
||||
setDraggedItem(item)
|
||||
}
|
||||
|
||||
const stopPropagating = (e: React.MouseEvent) => e.stopPropagation()
|
||||
|
||||
const handlePushElementRef =
|
||||
(idx: number) => (elem: HTMLDivElement | null) => {
|
||||
elem && (placeholderRefs.current[idx] = elem)
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
flex={1}
|
||||
spacing={1}
|
||||
maxW="full"
|
||||
onClick={stopPropagating}
|
||||
pointerEvents={isReadOnly ? 'none' : 'all'}
|
||||
>
|
||||
<Flex
|
||||
ref={handlePushElementRef(0)}
|
||||
h={showPlaceholders && expandedPlaceholderIndex === 0 ? '50px' : '2px'}
|
||||
bgColor={'gray.300'}
|
||||
visibility={showPlaceholders ? 'visible' : 'hidden'}
|
||||
rounded="lg"
|
||||
transition={showPlaceholders ? 'height 200ms' : 'none'}
|
||||
/>
|
||||
{step.items.map((item, idx) => (
|
||||
<Stack key={item.id} spacing={1}>
|
||||
<ItemNode
|
||||
item={item}
|
||||
indices={{ blockIndex, stepIndex, itemIndex: idx }}
|
||||
onMouseDown={handleStepMouseDown(idx)}
|
||||
isReadOnly={isReadOnly}
|
||||
isLastItem={idx === step.items.length - 1}
|
||||
/>
|
||||
<Flex
|
||||
ref={handlePushElementRef(idx + 1)}
|
||||
h={
|
||||
showPlaceholders && expandedPlaceholderIndex === idx + 1
|
||||
? '50px'
|
||||
: '2px'
|
||||
}
|
||||
bgColor={'gray.300'}
|
||||
visibility={showPlaceholders ? 'visible' : 'hidden'}
|
||||
rounded="lg"
|
||||
transition={showPlaceholders ? 'height 200ms' : 'none'}
|
||||
/>
|
||||
</Stack>
|
||||
))}
|
||||
<Stack>
|
||||
<Flex
|
||||
px="4"
|
||||
py="2"
|
||||
borderWidth="1px"
|
||||
borderColor="gray.300"
|
||||
bgColor={isReadOnly ? '' : 'gray.50'}
|
||||
rounded="md"
|
||||
pos="relative"
|
||||
align="center"
|
||||
cursor={isReadOnly ? 'pointer' : 'not-allowed'}
|
||||
>
|
||||
<Text color={isReadOnly ? 'inherit' : 'gray.500'}>Default</Text>
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
blockId: step.blockId,
|
||||
stepId: step.id,
|
||||
}}
|
||||
pos="absolute"
|
||||
right="15px"
|
||||
/>
|
||||
</Flex>
|
||||
</Stack>
|
||||
|
||||
{draggedItem && draggedItem.stepId === step.id && (
|
||||
<Portal>
|
||||
<ItemNodeOverlay
|
||||
item={draggedItem}
|
||||
pos="fixed"
|
||||
top="0"
|
||||
left="0"
|
||||
style={{
|
||||
transform: `translate(${position.x}px, ${position.y}px) rotate(-2deg)`,
|
||||
}}
|
||||
/>
|
||||
</Portal>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { ItemNodesList } from './ItemNodesList'
|
@ -8,13 +8,17 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import { ExpandIcon } from 'assets/icons'
|
||||
import {
|
||||
ConditionItem,
|
||||
ConditionStep,
|
||||
InputStepType,
|
||||
IntegrationStepType,
|
||||
LogicStepType,
|
||||
Step,
|
||||
StepIndices,
|
||||
StepOptions,
|
||||
TextBubbleStep,
|
||||
Webhook,
|
||||
WebhookStep,
|
||||
} from 'models'
|
||||
import { useRef } from 'react'
|
||||
import {
|
||||
@ -37,9 +41,9 @@ type Props = {
|
||||
step: Exclude<Step, TextBubbleStep>
|
||||
webhook?: Webhook
|
||||
onExpandClick: () => void
|
||||
onOptionsChange: (options: StepOptions) => void
|
||||
onWebhookChange: (updates: Partial<Webhook>) => void
|
||||
onStepChange: (updates: Partial<Step>) => void
|
||||
onTestRequestClick: () => void
|
||||
indices: StepIndices
|
||||
}
|
||||
|
||||
export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
|
||||
@ -79,23 +83,35 @@ export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
|
||||
|
||||
export const StepSettings = ({
|
||||
step,
|
||||
webhook,
|
||||
onOptionsChange,
|
||||
onWebhookChange,
|
||||
onStepChange,
|
||||
onTestRequestClick,
|
||||
indices,
|
||||
}: {
|
||||
step: Step
|
||||
webhook?: Webhook
|
||||
onOptionsChange: (options: StepOptions) => void
|
||||
onWebhookChange: (updates: Partial<Webhook>) => void
|
||||
onStepChange: (step: Partial<Step>) => void
|
||||
onTestRequestClick: () => void
|
||||
indices: StepIndices
|
||||
}) => {
|
||||
const handleOptionsChange = (options: StepOptions) => {
|
||||
onStepChange({ options } as Partial<Step>)
|
||||
}
|
||||
const handleWebhookChange = (updates: Partial<Webhook>) => {
|
||||
onStepChange({
|
||||
webhook: { ...(step as WebhookStep).webhook, ...updates },
|
||||
} as Partial<Step>)
|
||||
}
|
||||
const handleItemChange = (updates: Partial<ConditionItem>) => {
|
||||
onStepChange({
|
||||
items: [{ ...(step as ConditionStep).items[0], ...updates }],
|
||||
} as Partial<Step>)
|
||||
}
|
||||
switch (step.type) {
|
||||
case InputStepType.TEXT: {
|
||||
return (
|
||||
<TextInputSettingsBody
|
||||
options={step.options}
|
||||
onOptionsChange={onOptionsChange}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -103,7 +119,7 @@ export const StepSettings = ({
|
||||
return (
|
||||
<NumberInputSettingsBody
|
||||
options={step.options}
|
||||
onOptionsChange={onOptionsChange}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -111,7 +127,7 @@ export const StepSettings = ({
|
||||
return (
|
||||
<EmailInputSettingsBody
|
||||
options={step.options}
|
||||
onOptionsChange={onOptionsChange}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -119,7 +135,7 @@ export const StepSettings = ({
|
||||
return (
|
||||
<UrlInputSettingsBody
|
||||
options={step.options}
|
||||
onOptionsChange={onOptionsChange}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -127,7 +143,7 @@ export const StepSettings = ({
|
||||
return (
|
||||
<DateInputSettingsBody
|
||||
options={step.options}
|
||||
onOptionsChange={onOptionsChange}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -135,7 +151,7 @@ export const StepSettings = ({
|
||||
return (
|
||||
<PhoneNumberSettingsBody
|
||||
options={step.options}
|
||||
onOptionsChange={onOptionsChange}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -143,7 +159,7 @@ export const StepSettings = ({
|
||||
return (
|
||||
<ChoiceInputSettingsBody
|
||||
options={step.options}
|
||||
onOptionsChange={onOptionsChange}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -151,23 +167,20 @@ export const StepSettings = ({
|
||||
return (
|
||||
<SetVariableSettings
|
||||
options={step.options}
|
||||
onOptionsChange={onOptionsChange}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case LogicStepType.CONDITION: {
|
||||
return (
|
||||
<ConditionSettingsBody
|
||||
options={step.options}
|
||||
onOptionsChange={onOptionsChange}
|
||||
/>
|
||||
<ConditionSettingsBody step={step} onItemChange={handleItemChange} />
|
||||
)
|
||||
}
|
||||
case LogicStepType.REDIRECT: {
|
||||
return (
|
||||
<RedirectSettings
|
||||
options={step.options}
|
||||
onOptionsChange={onOptionsChange}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -175,7 +188,7 @@ export const StepSettings = ({
|
||||
return (
|
||||
<GoogleSheetsSettingsBody
|
||||
options={step.options}
|
||||
onOptionsChange={onOptionsChange}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
stepId={step.id}
|
||||
/>
|
||||
)
|
||||
@ -184,18 +197,18 @@ export const StepSettings = ({
|
||||
return (
|
||||
<GoogleAnalyticsSettings
|
||||
options={step.options}
|
||||
onOptionsChange={onOptionsChange}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case IntegrationStepType.WEBHOOK: {
|
||||
return (
|
||||
<WebhookSettings
|
||||
options={step.options}
|
||||
webhook={webhook as Webhook}
|
||||
onOptionsChange={onOptionsChange}
|
||||
onWebhookChange={onWebhookChange}
|
||||
step={step}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onWebhookChange={handleWebhookChange}
|
||||
onTestRequestClick={onTestRequestClick}
|
||||
indices={indices}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -1,33 +1,40 @@
|
||||
import { Flex } from '@chakra-ui/react'
|
||||
import { DropdownList } from 'components/shared/DropdownList'
|
||||
import { TableList } from 'components/shared/TableList'
|
||||
import { Comparison, ConditionOptions, LogicalOperator, Table } from 'models'
|
||||
import {
|
||||
Comparison,
|
||||
ConditionItem,
|
||||
ConditionStep,
|
||||
LogicalOperator,
|
||||
} from 'models'
|
||||
import React from 'react'
|
||||
import { ComparisonItem } from './ComparisonsItem'
|
||||
|
||||
type ConditionSettingsBodyProps = {
|
||||
options: ConditionOptions
|
||||
onOptionsChange: (options: ConditionOptions) => void
|
||||
step: ConditionStep
|
||||
onItemChange: (updates: Partial<ConditionItem>) => void
|
||||
}
|
||||
|
||||
export const ConditionSettingsBody = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
step,
|
||||
onItemChange,
|
||||
}: ConditionSettingsBodyProps) => {
|
||||
const handleComparisonsChange = (comparisons: Table<Comparison>) =>
|
||||
onOptionsChange({ ...options, comparisons })
|
||||
const itemContent = step.items[0].content
|
||||
|
||||
const handleComparisonsChange = (comparisons: Comparison[]) =>
|
||||
onItemChange({ content: { ...itemContent, comparisons } })
|
||||
const handleLogicalOperatorChange = (logicalOperator: LogicalOperator) =>
|
||||
onOptionsChange({ ...options, logicalOperator })
|
||||
onItemChange({ content: { ...itemContent, logicalOperator } })
|
||||
|
||||
return (
|
||||
<TableList<Comparison>
|
||||
initialItems={options.comparisons}
|
||||
initialItems={itemContent.comparisons}
|
||||
onItemsChange={handleComparisonsChange}
|
||||
Item={ComparisonItem}
|
||||
ComponentBetweenItems={() => (
|
||||
<Flex justify="center">
|
||||
<DropdownList<LogicalOperator>
|
||||
currentItem={options.logicalOperator}
|
||||
currentItem={itemContent.logicalOperator}
|
||||
onItemSelect={handleLogicalOperatorChange}
|
||||
items={Object.values(LogicalOperator)}
|
||||
/>
|
||||
|
@ -6,14 +6,12 @@ import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { CredentialsType } from 'db'
|
||||
import {
|
||||
Cell,
|
||||
defaultTable,
|
||||
ExtractingCell,
|
||||
GoogleSheetsAction,
|
||||
GoogleSheetsGetOptions,
|
||||
GoogleSheetsInsertRowOptions,
|
||||
GoogleSheetsOptions,
|
||||
GoogleSheetsUpdateRowOptions,
|
||||
Table,
|
||||
} from 'models'
|
||||
import React, { useMemo } from 'react'
|
||||
import {
|
||||
@ -60,7 +58,7 @@ export const GoogleSheetsSettingsBody = ({
|
||||
const newOptions: GoogleSheetsGetOptions = {
|
||||
...options,
|
||||
action,
|
||||
cellsToExtract: defaultTable,
|
||||
cellsToExtract: [],
|
||||
}
|
||||
return onOptionsChange({ ...newOptions })
|
||||
}
|
||||
@ -68,7 +66,7 @@ export const GoogleSheetsSettingsBody = ({
|
||||
const newOptions: GoogleSheetsInsertRowOptions = {
|
||||
...options,
|
||||
action,
|
||||
cellsToInsert: defaultTable,
|
||||
cellsToInsert: [],
|
||||
}
|
||||
return onOptionsChange({ ...newOptions })
|
||||
}
|
||||
@ -76,7 +74,7 @@ export const GoogleSheetsSettingsBody = ({
|
||||
const newOptions: GoogleSheetsUpdateRowOptions = {
|
||||
...options,
|
||||
action,
|
||||
cellsToUpsert: defaultTable,
|
||||
cellsToUpsert: [],
|
||||
}
|
||||
return onOptionsChange({ ...newOptions })
|
||||
}
|
||||
@ -155,16 +153,16 @@ const ActionOptions = ({
|
||||
sheet: Sheet
|
||||
onOptionsChange: (options: GoogleSheetsOptions) => void
|
||||
}) => {
|
||||
const handleInsertColumnsChange = (cellsToInsert: Table<Cell>) =>
|
||||
const handleInsertColumnsChange = (cellsToInsert: Cell[]) =>
|
||||
onOptionsChange({ ...options, cellsToInsert } as GoogleSheetsOptions)
|
||||
|
||||
const handleUpsertColumnsChange = (cellsToUpsert: Table<Cell>) =>
|
||||
const handleUpsertColumnsChange = (cellsToUpsert: Cell[]) =>
|
||||
onOptionsChange({ ...options, cellsToUpsert } as GoogleSheetsOptions)
|
||||
|
||||
const handleReferenceCellChange = (referenceCell: Cell) =>
|
||||
onOptionsChange({ ...options, referenceCell } as GoogleSheetsOptions)
|
||||
|
||||
const handleExtractingCellsChange = (cellsToExtract: Table<ExtractingCell>) =>
|
||||
const handleExtractingCellsChange = (cellsToExtract: ExtractingCell[]) =>
|
||||
onOptionsChange({ ...options, cellsToExtract } as GoogleSheetsOptions)
|
||||
|
||||
const UpdatingCellItem = useMemo(
|
||||
@ -194,9 +192,8 @@ const ActionOptions = ({
|
||||
<Stack>
|
||||
<Text>Row to select</Text>
|
||||
<CellWithValueStack
|
||||
id={'reference'}
|
||||
columns={sheet.columns}
|
||||
item={options.referenceCell ?? {}}
|
||||
item={options.referenceCell ?? { id: 'reference' }}
|
||||
onItemChange={handleReferenceCellChange}
|
||||
/>
|
||||
<Text>Cells to update</Text>
|
||||
@ -213,9 +210,8 @@ const ActionOptions = ({
|
||||
<Stack>
|
||||
<Text>Row to select</Text>
|
||||
<CellWithValueStack
|
||||
id={'reference'}
|
||||
columns={sheet.columns}
|
||||
item={options.referenceCell ?? {}}
|
||||
item={options.referenceCell ?? { id: 'reference' }}
|
||||
onItemChange={handleReferenceCellChange}
|
||||
/>
|
||||
<Text>Cells to extract</Text>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { DebouncedInput } from 'components/shared/DebouncedInput'
|
||||
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||
import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton'
|
||||
import { RedirectOptions } from 'models'
|
||||
|
@ -20,7 +20,6 @@ export const HeadersInputs = (props: TableListItemProps<KeyValue>) => (
|
||||
)
|
||||
|
||||
export const KeyValueInputs = ({
|
||||
id,
|
||||
item,
|
||||
onItemChange,
|
||||
keyPlaceholder,
|
||||
@ -40,18 +39,18 @@ export const KeyValueInputs = ({
|
||||
return (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<FormControl>
|
||||
<FormLabel htmlFor={'key' + id}>Key:</FormLabel>
|
||||
<FormLabel htmlFor={'key' + item.id}>Key:</FormLabel>
|
||||
<InputWithVariableButton
|
||||
id={'key' + id}
|
||||
id={'key' + item.id}
|
||||
initialValue={item.key ?? ''}
|
||||
onChange={handleKeyChange}
|
||||
placeholder={keyPlaceholder}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor={'value' + id}>Value:</FormLabel>
|
||||
<FormLabel htmlFor={'value' + item.id}>Value:</FormLabel>
|
||||
<InputWithVariableButton
|
||||
id={'value' + id}
|
||||
id={'value' + item.id}
|
||||
initialValue={item.value ?? ''}
|
||||
onChange={handleValueChange}
|
||||
placeholder={valuePlaceholder}
|
||||
|
@ -5,7 +5,6 @@ import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { VariableForTest, Variable } from 'models'
|
||||
|
||||
export const VariableForTestInputs = ({
|
||||
id,
|
||||
item,
|
||||
onItemChange,
|
||||
}: TableListItemProps<VariableForTest>) => {
|
||||
@ -18,17 +17,17 @@ export const VariableForTestInputs = ({
|
||||
return (
|
||||
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
|
||||
<FormControl>
|
||||
<FormLabel htmlFor={'name' + id}>Variable name:</FormLabel>
|
||||
<FormLabel htmlFor={'name' + item.id}>Variable name:</FormLabel>
|
||||
<VariableSearchInput
|
||||
id={'name' + id}
|
||||
id={'name' + item.id}
|
||||
initialVariableId={item.variableId}
|
||||
onSelectVariable={handleVariableSelect}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor={'value' + id}>Test value:</FormLabel>
|
||||
<FormLabel htmlFor={'value' + item.id}>Test value:</FormLabel>
|
||||
<DebouncedInput
|
||||
id={'value' + id}
|
||||
id={'value' + item.id}
|
||||
initialValue={item.value ?? ''}
|
||||
onChange={handleValueChange}
|
||||
/>
|
||||
|
@ -15,11 +15,12 @@ import { useTypebot } from 'contexts/TypebotContext'
|
||||
import {
|
||||
HttpMethod,
|
||||
KeyValue,
|
||||
Table,
|
||||
WebhookOptions,
|
||||
VariableForTest,
|
||||
Webhook,
|
||||
ResponseVariableMapping,
|
||||
WebhookStep,
|
||||
StepIndices,
|
||||
} from 'models'
|
||||
import { DropdownList } from 'components/shared/DropdownList'
|
||||
import { TableList, TableListItemProps } from 'components/shared/TableList'
|
||||
@ -34,19 +35,19 @@ import { VariableForTestInputs } from './VariableForTestInputs'
|
||||
import { DataVariableInputs } from './ResponseMappingInputs'
|
||||
|
||||
type Props = {
|
||||
webhook: Webhook
|
||||
options?: WebhookOptions
|
||||
step: WebhookStep
|
||||
onOptionsChange: (options: WebhookOptions) => void
|
||||
onWebhookChange: (updates: Partial<Webhook>) => void
|
||||
onTestRequestClick: () => void
|
||||
indices: StepIndices
|
||||
}
|
||||
|
||||
export const WebhookSettings = ({
|
||||
options,
|
||||
step: { webhook, options },
|
||||
onOptionsChange,
|
||||
webhook,
|
||||
onWebhookChange,
|
||||
onTestRequestClick,
|
||||
indices,
|
||||
}: Props) => {
|
||||
const { typebot, save } = useTypebot()
|
||||
const [isTestResponseLoading, setIsTestResponseLoading] = useState(false)
|
||||
@ -62,23 +63,23 @@ export const WebhookSettings = ({
|
||||
|
||||
const handleMethodChange = (method: HttpMethod) => onWebhookChange({ method })
|
||||
|
||||
const handleQueryParamsChange = (queryParams: Table<KeyValue>) =>
|
||||
const handleQueryParamsChange = (queryParams: KeyValue[]) =>
|
||||
onWebhookChange({ queryParams })
|
||||
|
||||
const handleHeadersChange = (headers: Table<KeyValue>) =>
|
||||
const handleHeadersChange = (headers: KeyValue[]) =>
|
||||
onWebhookChange({ headers })
|
||||
|
||||
const handleBodyChange = (body: string) => onWebhookChange({ body })
|
||||
|
||||
const handleVariablesChange = (variablesForTest: Table<VariableForTest>) =>
|
||||
options && onOptionsChange({ ...options, variablesForTest })
|
||||
const handleVariablesChange = (variablesForTest: VariableForTest[]) =>
|
||||
onOptionsChange({ ...options, variablesForTest })
|
||||
|
||||
const handleResponseMappingChange = (
|
||||
responseVariableMapping: Table<ResponseVariableMapping>
|
||||
) => options && onOptionsChange({ ...options, responseVariableMapping })
|
||||
responseVariableMapping: ResponseVariableMapping[]
|
||||
) => onOptionsChange({ ...options, responseVariableMapping })
|
||||
|
||||
const handleTestRequestClick = async () => {
|
||||
if (!typebot || !webhook) return
|
||||
if (!typebot) return
|
||||
setIsTestResponseLoading(true)
|
||||
onTestRequestClick()
|
||||
await save()
|
||||
@ -86,9 +87,10 @@ export const WebhookSettings = ({
|
||||
typebot.id,
|
||||
webhook.id,
|
||||
convertVariableForTestToVariables(
|
||||
options?.variablesForTest,
|
||||
options.variablesForTest,
|
||||
typebot.variables
|
||||
)
|
||||
),
|
||||
indices
|
||||
)
|
||||
if (error) return toast({ title: error.name, description: error.message })
|
||||
setTestResponse(JSON.stringify(data, undefined, 2))
|
||||
@ -196,9 +198,7 @@ export const WebhookSettings = ({
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<ResponseVariableMapping>
|
||||
initialItems={
|
||||
options?.responseVariableMapping ?? { byId: {}, allIds: [] }
|
||||
}
|
||||
initialItems={options.responseVariableMapping}
|
||||
onItemsChange={handleResponseMappingChange}
|
||||
Item={ResponseMappingInputs}
|
||||
addLabel="Add an entry"
|
||||
|
@ -4,21 +4,19 @@ import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
useDisclosure,
|
||||
useEventListener,
|
||||
} from '@chakra-ui/react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
BubbleStep,
|
||||
BubbleStepContent,
|
||||
DraggableStep,
|
||||
Step,
|
||||
StepOptions,
|
||||
TextBubbleContent,
|
||||
TextBubbleStep,
|
||||
Webhook,
|
||||
} from 'models'
|
||||
import { Coordinates, useGraph } from 'contexts/GraphContext'
|
||||
import { useGraph } from 'contexts/GraphContext'
|
||||
import { StepIcon } from 'components/editor/StepsSideBar/StepIcon'
|
||||
import { isBubbleStep, isTextBubbleStep, isWebhookStep } from 'utils'
|
||||
import { isBubbleStep, isTextBubbleStep, stepHasItems } from 'utils'
|
||||
import { StepNodeContent } from './StepNodeContent/StepNodeContent'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { ContextMenu } from 'components/shared/ContextMenu'
|
||||
@ -32,46 +30,41 @@ import { StepSettings } from './SettingsPopoverContent/SettingsPopoverContent'
|
||||
import { TextBubbleEditor } from './TextBubbleEditor'
|
||||
import { TargetEndpoint } from '../../Endpoints'
|
||||
import { MediaBubblePopoverContent } from './MediaBubblePopoverContent'
|
||||
import { NodePosition, useDragDistance } from 'contexts/GraphDndContext'
|
||||
import { setMultipleRefs } from 'services/utils'
|
||||
|
||||
export const StepNode = ({
|
||||
step,
|
||||
isConnectable,
|
||||
onMouseMoveBottomOfElement,
|
||||
onMouseMoveTopOfElement,
|
||||
indices,
|
||||
onMouseDown,
|
||||
}: {
|
||||
step: Step
|
||||
isConnectable: boolean
|
||||
onMouseMoveBottomOfElement?: () => void
|
||||
onMouseMoveTopOfElement?: () => void
|
||||
onMouseDown?: (
|
||||
stepNodePosition: { absolute: Coordinates; relative: Coordinates },
|
||||
step: DraggableStep
|
||||
) => void
|
||||
indices: { stepIndex: number; blockIndex: number }
|
||||
onMouseDown?: (stepNodePosition: NodePosition, step: DraggableStep) => void
|
||||
}) => {
|
||||
const { query } = useRouter()
|
||||
const {
|
||||
setConnectingIds,
|
||||
connectingIds,
|
||||
openedStepId,
|
||||
setOpenedStepId,
|
||||
blocksCoordinates,
|
||||
} = useGraph()
|
||||
const { detachStepFromBlock, updateStep, typebot, updateWebhook } =
|
||||
useTypebot()
|
||||
const { setConnectingIds, connectingIds, openedStepId, setOpenedStepId } =
|
||||
useGraph()
|
||||
const { updateStep } = useTypebot()
|
||||
const [localStep, setLocalStep] = useState(step)
|
||||
const [localWebhook, setLocalWebhook] = useState(
|
||||
isWebhookStep(step)
|
||||
? typebot?.webhooks.byId[step.options.webhookId ?? '']
|
||||
: undefined
|
||||
)
|
||||
const [isConnecting, setIsConnecting] = useState(false)
|
||||
const [isPopoverOpened, setIsPopoverOpened] = useState(
|
||||
openedStepId === step.id
|
||||
)
|
||||
const stepRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const onDrag = (position: NodePosition) => {
|
||||
if (step.type === 'start' || !onMouseDown) return
|
||||
onMouseDown(position, step)
|
||||
}
|
||||
useDragDistance({
|
||||
ref: stepRef,
|
||||
onDrag,
|
||||
isDisabled: !onMouseDown || step.type === 'start',
|
||||
})
|
||||
|
||||
const [mouseDownEvent, setMouseDownEvent] =
|
||||
useState<{ absolute: Coordinates; relative: Coordinates }>()
|
||||
const [isEditing, setIsEditing] = useState<boolean>(
|
||||
isTextBubbleStep(step) && step.content.plainText === ''
|
||||
)
|
||||
@ -98,15 +91,15 @@ export const StepNode = ({
|
||||
}, [connectingIds, step.blockId, step.id])
|
||||
|
||||
const handleModalClose = () => {
|
||||
updateStep(localStep.id, { ...localStep })
|
||||
updateStep(indices, { ...localStep })
|
||||
onModalClose()
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (connectingIds?.target)
|
||||
if (connectingIds)
|
||||
setConnectingIds({
|
||||
...connectingIds,
|
||||
target: { ...connectingIds.target, stepId: step.id },
|
||||
target: { blockId: step.blockId, stepId: step.id },
|
||||
})
|
||||
}
|
||||
|
||||
@ -118,54 +111,16 @@ export const StepNode = ({
|
||||
})
|
||||
}
|
||||
|
||||
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 handleMouseUp = () => {
|
||||
if (mouseDownEvent) {
|
||||
setIsEditing(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!onMouseMoveBottomOfElement || !onMouseMoveTopOfElement) return
|
||||
const isMovingAndIsMouseDown =
|
||||
mouseDownEvent &&
|
||||
onMouseDown &&
|
||||
(event.movementX > 0 || event.movementY > 0)
|
||||
if (isMovingAndIsMouseDown && step.type !== 'start') {
|
||||
onMouseDown(mouseDownEvent, step)
|
||||
detachStepFromBlock(step.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 handleCloseEditor = () => {
|
||||
const handleCloseEditor = (content: TextBubbleContent) => {
|
||||
const updatedStep = { ...localStep, content } as Step
|
||||
setLocalStep(updatedStep)
|
||||
updateStep(indices, updatedStep)
|
||||
setIsEditing(false)
|
||||
}
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (isTextBubbleStep(step)) setIsEditing(true)
|
||||
setOpenedStepId(step.id)
|
||||
}
|
||||
|
||||
@ -175,22 +130,16 @@ export const StepNode = ({
|
||||
}
|
||||
|
||||
const updateOptions = () => {
|
||||
updateStep(localStep.id, { ...localStep })
|
||||
if (localWebhook) updateWebhook(localWebhook.id, { ...localWebhook })
|
||||
updateStep(indices, { ...localStep })
|
||||
}
|
||||
|
||||
const handleOptionsChange = (options: StepOptions) => {
|
||||
setLocalStep({ ...localStep, options } as Step)
|
||||
const handleStepChange = (updates: Partial<Step>) => {
|
||||
setLocalStep({ ...localStep, ...updates } as Step)
|
||||
}
|
||||
|
||||
const handleContentChange = (content: BubbleStepContent) =>
|
||||
setLocalStep({ ...localStep, content } as Step)
|
||||
|
||||
const handleWebhookChange = (updates: Partial<Webhook>) => {
|
||||
if (!localWebhook) return
|
||||
setLocalWebhook({ ...localWebhook, ...updates })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isPopoverOpened && openedStepId !== step.id) updateOptions()
|
||||
setIsPopoverOpened(openedStepId === step.id)
|
||||
@ -199,13 +148,12 @@ export const StepNode = ({
|
||||
|
||||
return isEditing && isTextBubbleStep(localStep) ? (
|
||||
<TextBubbleEditor
|
||||
stepId={localStep.id}
|
||||
initialValue={localStep.content.richText}
|
||||
onClose={handleCloseEditor}
|
||||
/>
|
||||
) : (
|
||||
<ContextMenu<HTMLDivElement>
|
||||
renderMenu={() => <StepNodeContextMenu stepId={step.id} />}
|
||||
renderMenu={() => <StepNodeContextMenu indices={indices} />}
|
||||
>
|
||||
{(ref, isOpened) => (
|
||||
<Popover
|
||||
@ -217,14 +165,11 @@ export const StepNode = ({
|
||||
<PopoverTrigger>
|
||||
<Flex
|
||||
pos="relative"
|
||||
ref={ref}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseDown={handleMouseDown}
|
||||
ref={setMultipleRefs([ref, stepRef])}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onMouseUp={handleMouseUp}
|
||||
onClick={handleClick}
|
||||
data-testid={`step-${step.id}`}
|
||||
data-testid={`step`}
|
||||
w="full"
|
||||
>
|
||||
<HStack
|
||||
@ -244,37 +189,34 @@ export const StepNode = ({
|
||||
mt="1"
|
||||
data-testid={`${localStep.id}-icon`}
|
||||
/>
|
||||
<StepNodeContent step={localStep} />
|
||||
<StepNodeContent step={localStep} indices={indices} />
|
||||
<TargetEndpoint
|
||||
pos="absolute"
|
||||
left="-32px"
|
||||
top="19px"
|
||||
stepId={localStep.id}
|
||||
/>
|
||||
{blocksCoordinates &&
|
||||
isConnectable &&
|
||||
hasDefaultConnector(localStep) && (
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
blockId: localStep.blockId,
|
||||
stepId: localStep.id,
|
||||
}}
|
||||
pos="absolute"
|
||||
right="15px"
|
||||
bottom="18px"
|
||||
/>
|
||||
)}
|
||||
{isConnectable && hasDefaultConnector(localStep) && (
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
blockId: localStep.blockId,
|
||||
stepId: localStep.id,
|
||||
}}
|
||||
pos="absolute"
|
||||
right="15px"
|
||||
bottom="18px"
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
</Flex>
|
||||
</PopoverTrigger>
|
||||
{hasSettingsPopover(localStep) && (
|
||||
<SettingsPopoverContent
|
||||
step={localStep}
|
||||
webhook={localWebhook}
|
||||
onExpandClick={handleExpandClick}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onWebhookChange={handleWebhookChange}
|
||||
onStepChange={handleStepChange}
|
||||
onTestRequestClick={updateOptions}
|
||||
indices={indices}
|
||||
/>
|
||||
)}
|
||||
{isMediaBubbleStep(localStep) && (
|
||||
@ -286,10 +228,9 @@ export const StepNode = ({
|
||||
<SettingsModal isOpen={isModalOpen} onClose={handleModalClose}>
|
||||
<StepSettings
|
||||
step={localStep}
|
||||
webhook={localWebhook}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onWebhookChange={handleWebhookChange}
|
||||
onStepChange={handleStepChange}
|
||||
onTestRequestClick={updateOptions}
|
||||
indices={indices}
|
||||
/>
|
||||
</SettingsModal>
|
||||
</Popover>
|
||||
|
@ -6,11 +6,11 @@ import {
|
||||
InputStepType,
|
||||
LogicStepType,
|
||||
IntegrationStepType,
|
||||
StepIndices,
|
||||
} from 'models'
|
||||
import { isInputStep } from 'utils'
|
||||
import { ButtonNodesList } from '../../ButtonNode'
|
||||
import { ItemNodesList } from '../../ItemNode'
|
||||
import {
|
||||
ConditionContent,
|
||||
SetVariableContent,
|
||||
TextBubbleContent,
|
||||
VideoBubbleContent,
|
||||
@ -23,9 +23,9 @@ import { PlaceholderContent } from './contents/PlaceholderContent'
|
||||
|
||||
type Props = {
|
||||
step: Step | StartStep
|
||||
isConnectable?: boolean
|
||||
indices: StepIndices
|
||||
}
|
||||
export const StepNodeContent = ({ step }: Props) => {
|
||||
export const StepNodeContent = ({ step, indices }: Props) => {
|
||||
if (isInputStep(step) && step.options.variableId) {
|
||||
return <WithVariableContent step={step} />
|
||||
}
|
||||
@ -52,13 +52,13 @@ export const StepNodeContent = ({ step }: Props) => {
|
||||
return <Text color={'gray.500'}>Pick a date...</Text>
|
||||
}
|
||||
case InputStepType.CHOICE: {
|
||||
return <ButtonNodesList step={step} />
|
||||
return <ItemNodesList step={step} indices={indices} />
|
||||
}
|
||||
case LogicStepType.SET_VARIABLE: {
|
||||
return <SetVariableContent step={step} />
|
||||
}
|
||||
case LogicStepType.CONDITION: {
|
||||
return <ConditionContent step={step} />
|
||||
return <ItemNodesList step={step} indices={indices} isReadOnly />
|
||||
}
|
||||
case LogicStepType.REDIRECT: {
|
||||
return (
|
||||
|
@ -1,57 +0,0 @@
|
||||
import { Flex, Stack, HStack, Tag, Text } from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { ConditionStep } from 'models'
|
||||
import { SourceEndpoint } from '../../../../Endpoints/SourceEndpoint'
|
||||
|
||||
export const ConditionContent = ({ step }: { step: ConditionStep }) => {
|
||||
const { typebot } = useTypebot()
|
||||
return (
|
||||
<Flex>
|
||||
{step.options?.comparisons.allIds.length === 0 ? (
|
||||
<Text color={'gray.500'}>Configure...</Text>
|
||||
) : (
|
||||
<Stack>
|
||||
{step.options?.comparisons.allIds.map((comparisonId, idx) => {
|
||||
const comparison = step.options?.comparisons.byId[comparisonId]
|
||||
const variable =
|
||||
typebot?.variables.byId[comparison?.variableId ?? '']
|
||||
return (
|
||||
<HStack key={comparisonId} spacing={1}>
|
||||
{idx > 0 && <Text>{step.options?.logicalOperator ?? ''}</Text>}
|
||||
{variable?.name && (
|
||||
<Tag bgColor="orange.400">{variable.name}</Tag>
|
||||
)}
|
||||
{comparison.comparisonOperator && (
|
||||
<Text>{comparison?.comparisonOperator}</Text>
|
||||
)}
|
||||
{comparison?.value && (
|
||||
<Tag bgColor={'green.400'}>{comparison.value}</Tag>
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
)}
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
blockId: step.blockId,
|
||||
stepId: step.id,
|
||||
conditionType: 'true',
|
||||
}}
|
||||
pos="absolute"
|
||||
top="7px"
|
||||
right="15px"
|
||||
/>
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
blockId: step.blockId,
|
||||
stepId: step.id,
|
||||
conditionType: 'false',
|
||||
}}
|
||||
pos="absolute"
|
||||
bottom="7px"
|
||||
right="15px"
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { SetVariableStep } from 'models'
|
||||
import { byId } from 'utils'
|
||||
|
||||
export const SetVariableContent = ({ step }: { step: SetVariableStep }) => {
|
||||
const { typebot } = useTypebot()
|
||||
const variableName =
|
||||
typebot?.variables.byId[step.options?.variableId ?? '']?.name ?? ''
|
||||
const expression = step.options?.expressionToEvaluate ?? ''
|
||||
typebot?.variables.find(byId(step.options.variableId))?.name ?? ''
|
||||
const expression = step.options.expressionToEvaluate ?? ''
|
||||
return (
|
||||
<Text color={'gray.500'}>
|
||||
{variableName === '' && expression === ''
|
||||
|
@ -10,6 +10,7 @@ type Props = {
|
||||
|
||||
export const TextBubbleContent = ({ step }: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
if (!typebot) return <></>
|
||||
return (
|
||||
<Flex
|
||||
flexDir={'column'}
|
||||
|
@ -1,18 +1,11 @@
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { WebhookStep } from 'models'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
type Props = {
|
||||
step: WebhookStep
|
||||
}
|
||||
|
||||
export const WebhookContent = ({ step }: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
const webhook = useMemo(
|
||||
() => typebot?.webhooks.byId[step.options?.webhookId ?? ''],
|
||||
[step.options?.webhookId, typebot?.webhooks.byId]
|
||||
)
|
||||
export const WebhookContent = ({ step: { webhook } }: Props) => {
|
||||
if (!webhook?.url) return <Text color="gray.500">Configure...</Text>
|
||||
return (
|
||||
<Text isTruncated pr="6">
|
||||
|
@ -2,6 +2,7 @@ import { InputStep } from 'models'
|
||||
import { chakra, Text } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { byId } from 'utils'
|
||||
|
||||
type Props = {
|
||||
step: InputStep
|
||||
@ -9,8 +10,10 @@ type Props = {
|
||||
|
||||
export const WithVariableContent = ({ step }: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
const variableName =
|
||||
typebot?.variables.byId[step.options.variableId as string].name
|
||||
const variableName = typebot?.variables.find(
|
||||
byId(step.options.variableId)
|
||||
)?.name
|
||||
|
||||
return (
|
||||
<Text>
|
||||
Collect{' '}
|
||||
|
@ -1,4 +1,3 @@
|
||||
export * from './ConditionContent'
|
||||
export * from './SetVariableContent'
|
||||
export * from './WithVariableContent'
|
||||
export * from './VideoBubbleContent'
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { MenuList, MenuItem } from '@chakra-ui/react'
|
||||
import { TrashIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { StepIndices } from 'models'
|
||||
|
||||
export const StepNodeContextMenu = ({ stepId }: { stepId: string }) => {
|
||||
type Props = { indices: StepIndices }
|
||||
export const StepNodeContextMenu = ({ indices }: Props) => {
|
||||
const { deleteStep } = useTypebot()
|
||||
|
||||
const handleDeleteClick = () => deleteStep(stepId)
|
||||
const handleDeleteClick = () => deleteStep(indices)
|
||||
|
||||
return (
|
||||
<MenuList>
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { StackProps, HStack } from '@chakra-ui/react'
|
||||
import { StartStep, Step } from 'models'
|
||||
import { StartStep, Step, StepIndices } from 'models'
|
||||
import { StepIcon } from 'components/editor/StepsSideBar/StepIcon'
|
||||
import { StepNodeContent } from './StepNodeContent/StepNodeContent'
|
||||
|
||||
export const StepNodeOverlay = ({
|
||||
step,
|
||||
indices,
|
||||
...props
|
||||
}: { step: Step | StartStep } & StackProps) => {
|
||||
}: { step: Step | StartStep; indices: StepIndices } & StackProps) => {
|
||||
return (
|
||||
<HStack
|
||||
p="3"
|
||||
@ -20,7 +21,7 @@ export const StepNodeOverlay = ({
|
||||
{...props}
|
||||
>
|
||||
<StepIcon type={step.type} />
|
||||
<StepNodeContent step={step} />
|
||||
<StepNodeContent step={step} indices={indices} />
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
@ -1,105 +1,123 @@
|
||||
import { useEventListener, Stack, Flex, Portal } from '@chakra-ui/react'
|
||||
import { DraggableStep } from 'models'
|
||||
import { useStepDnd } from 'contexts/StepDndContext'
|
||||
import { DraggableStep, DraggableStepType, Step } from 'models'
|
||||
import {
|
||||
computeNearestPlaceholderIndex,
|
||||
useStepDnd,
|
||||
} from 'contexts/GraphDndContext'
|
||||
import { Coordinates, useGraph } from 'contexts/GraphContext'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { StepNode } from './StepNode'
|
||||
import { StepNodeOverlay } from './StepNodeOverlay'
|
||||
|
||||
type Props = {
|
||||
blockId: string
|
||||
steps: Step[]
|
||||
blockIndex: number
|
||||
blockRef: React.MutableRefObject<HTMLDivElement | null>
|
||||
isStartBlock: boolean
|
||||
}
|
||||
export const StepNodesList = ({
|
||||
blockId,
|
||||
stepIds,
|
||||
}: {
|
||||
blockId: string
|
||||
stepIds: string[]
|
||||
}) => {
|
||||
steps,
|
||||
blockIndex,
|
||||
blockRef,
|
||||
isStartBlock,
|
||||
}: Props) => {
|
||||
const {
|
||||
draggedStep,
|
||||
setDraggedStep,
|
||||
draggedStepType,
|
||||
mouseOverBlockId,
|
||||
mouseOverBlock,
|
||||
setDraggedStepType,
|
||||
setMouseOverBlockId,
|
||||
} = useStepDnd()
|
||||
const { typebot, createStep } = useTypebot()
|
||||
const { typebot, createStep, detachStepFromBlock } = useTypebot()
|
||||
const { isReadOnly } = useGraph()
|
||||
const [expandedPlaceholderIndex, setExpandedPlaceholderIndex] = useState<
|
||||
number | undefined
|
||||
>()
|
||||
const showSortPlaceholders = useMemo(
|
||||
() => mouseOverBlockId === blockId && (draggedStep || draggedStepType),
|
||||
[mouseOverBlockId, blockId, draggedStep, draggedStepType]
|
||||
)
|
||||
const placeholderRefs = useRef<HTMLDivElement[]>([])
|
||||
const [position, setPosition] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
const [relativeCoordinates, setRelativeCoordinates] = useState({ x: 0, y: 0 })
|
||||
const [mousePositionInElement, setMousePositionInElement] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
const isDraggingOnCurrentBlock =
|
||||
(draggedStep || draggedStepType) && mouseOverBlock?.id === blockId
|
||||
const showSortPlaceholders = !isStartBlock && (draggedStep || draggedStepType)
|
||||
|
||||
const handleStepMove = (event: MouseEvent) => {
|
||||
if (!draggedStep) return
|
||||
useEffect(() => {
|
||||
if (mouseOverBlock?.id !== blockId) setExpandedPlaceholderIndex(undefined)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mouseOverBlock?.id])
|
||||
|
||||
const handleMouseMoveGlobal = (event: MouseEvent) => {
|
||||
if (!draggedStep || draggedStep.blockId !== blockId) return
|
||||
const { clientX, clientY } = event
|
||||
setPosition({
|
||||
...position,
|
||||
x: clientX - relativeCoordinates.x,
|
||||
y: clientY - relativeCoordinates.y,
|
||||
x: clientX - mousePositionInElement.x,
|
||||
y: clientY - mousePositionInElement.y,
|
||||
})
|
||||
}
|
||||
useEventListener('mousemove', handleStepMove)
|
||||
useEventListener('mousemove', handleMouseMoveGlobal)
|
||||
|
||||
const handleMouseMove = (event: React.MouseEvent) => {
|
||||
if (!draggedStep) return
|
||||
const element = event.currentTarget as HTMLDivElement
|
||||
const rect = element.getBoundingClientRect()
|
||||
const y = event.clientY - rect.top
|
||||
if (y < 20) setExpandedPlaceholderIndex(0)
|
||||
const handleMouseMoveOnBlock = (event: MouseEvent) => {
|
||||
if (!isDraggingOnCurrentBlock) return
|
||||
setExpandedPlaceholderIndex(
|
||||
computeNearestPlaceholderIndex(event.pageY, placeholderRefs)
|
||||
)
|
||||
}
|
||||
useEventListener('mousemove', handleMouseMoveOnBlock, blockRef.current)
|
||||
|
||||
const handleMouseUp = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (expandedPlaceholderIndex === undefined) return
|
||||
e.stopPropagation()
|
||||
setMouseOverBlockId(undefined)
|
||||
const handleMouseUpOnBlock = (e: MouseEvent) => {
|
||||
setExpandedPlaceholderIndex(undefined)
|
||||
if (!draggedStep && !draggedStepType) return
|
||||
if (!isDraggingOnCurrentBlock) return
|
||||
const stepIndex = computeNearestPlaceholderIndex(e.clientY, placeholderRefs)
|
||||
createStep(
|
||||
blockId,
|
||||
draggedStep || draggedStepType,
|
||||
expandedPlaceholderIndex
|
||||
(draggedStep || draggedStepType) as DraggableStep | DraggableStepType,
|
||||
{
|
||||
blockIndex,
|
||||
stepIndex,
|
||||
}
|
||||
)
|
||||
setDraggedStep(undefined)
|
||||
setDraggedStepType(undefined)
|
||||
}
|
||||
useEventListener(
|
||||
'mouseup',
|
||||
handleMouseUpOnBlock,
|
||||
mouseOverBlock?.ref.current,
|
||||
{
|
||||
capture: true,
|
||||
}
|
||||
)
|
||||
|
||||
const handleStepMouseDown = (
|
||||
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
|
||||
step: DraggableStep
|
||||
) => {
|
||||
if (isReadOnly) return
|
||||
setPosition(absolute)
|
||||
setRelativeCoordinates(relative)
|
||||
setMouseOverBlockId(blockId)
|
||||
setDraggedStep(step)
|
||||
}
|
||||
const handleStepMouseDown =
|
||||
(stepIndex: number) =>
|
||||
(
|
||||
{ absolute, relative }: { absolute: Coordinates; relative: Coordinates },
|
||||
step: DraggableStep
|
||||
) => {
|
||||
if (isReadOnly) return
|
||||
detachStepFromBlock({ blockIndex, stepIndex })
|
||||
setPosition(absolute)
|
||||
setMousePositionInElement(relative)
|
||||
setDraggedStep(step)
|
||||
}
|
||||
|
||||
const handleMouseOnTopOfStep = (stepIndex: number) => () => {
|
||||
if (!draggedStep && !draggedStepType) return
|
||||
setExpandedPlaceholderIndex(stepIndex === 0 ? 0 : stepIndex)
|
||||
}
|
||||
|
||||
const handleMouseOnBottomOfStep = (stepIndex: number) => () => {
|
||||
if (!draggedStep && !draggedStepType) return
|
||||
setExpandedPlaceholderIndex(stepIndex + 1)
|
||||
}
|
||||
const handlePushElementRef =
|
||||
(idx: number) => (elem: HTMLDivElement | null) => {
|
||||
elem && (placeholderRefs.current[idx] = elem)
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
spacing={1}
|
||||
onMouseUpCapture={handleMouseUp}
|
||||
onMouseMove={handleMouseMove}
|
||||
transition="none"
|
||||
>
|
||||
<Stack spacing={1} transition="none">
|
||||
<Flex
|
||||
ref={handlePushElementRef(0)}
|
||||
h={
|
||||
showSortPlaceholders && expandedPlaceholderIndex === 0
|
||||
? '50px'
|
||||
@ -111,17 +129,17 @@ export const StepNodesList = ({
|
||||
transition={showSortPlaceholders ? 'height 200ms' : 'none'}
|
||||
/>
|
||||
{typebot &&
|
||||
stepIds.map((stepId, idx) => (
|
||||
<Stack key={stepId} spacing={1}>
|
||||
steps.map((step, idx) => (
|
||||
<Stack key={step.id} spacing={1}>
|
||||
<StepNode
|
||||
key={stepId}
|
||||
step={typebot.steps.byId[stepId]}
|
||||
isConnectable={!isReadOnly && stepIds.length - 1 === idx}
|
||||
onMouseMoveTopOfElement={handleMouseOnTopOfStep(idx)}
|
||||
onMouseMoveBottomOfElement={handleMouseOnBottomOfStep(idx)}
|
||||
onMouseDown={handleStepMouseDown}
|
||||
key={step.id}
|
||||
step={step}
|
||||
indices={{ blockIndex, stepIndex: idx }}
|
||||
isConnectable={!isReadOnly && steps.length - 1 === idx}
|
||||
onMouseDown={handleStepMouseDown(idx)}
|
||||
/>
|
||||
<Flex
|
||||
ref={handlePushElementRef(idx + 1)}
|
||||
h={
|
||||
showSortPlaceholders && expandedPlaceholderIndex === idx + 1
|
||||
? '50px'
|
||||
@ -138,6 +156,7 @@ export const StepNodesList = ({
|
||||
<Portal>
|
||||
<StepNodeOverlay
|
||||
step={draggedStep}
|
||||
indices={{ blockIndex, stepIndex: 0 }}
|
||||
pos="fixed"
|
||||
top="0"
|
||||
left="0"
|
||||
|
@ -8,21 +8,19 @@ import {
|
||||
withPlate,
|
||||
} from '@udecode/plate-core'
|
||||
import { editorStyle, platePlugins } from 'libs/plate'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { BaseSelection, createEditor, Transforms } from 'slate'
|
||||
import { ToolBar } from './ToolBar'
|
||||
import { parseHtmlStringToPlainText } from 'services/utils'
|
||||
import { TextBubbleStep, Variable } from 'models'
|
||||
import { defaultTextBubbleContent, TextBubbleContent, Variable } from 'models'
|
||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||
import { ReactEditor } from 'slate-react'
|
||||
|
||||
type Props = {
|
||||
stepId: string
|
||||
initialValue: TDescendant[]
|
||||
onClose: () => void
|
||||
onClose: (newContent: TextBubbleContent) => void
|
||||
}
|
||||
|
||||
export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => {
|
||||
export const TextBubbleEditor = ({ initialValue, onClose }: Props) => {
|
||||
const randomEditorId = useMemo(() => Math.random().toString(), [])
|
||||
const editor = useMemo(
|
||||
() =>
|
||||
@ -30,7 +28,6 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
)
|
||||
const { updateStep } = useTypebot()
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const varDropdownRef = useRef<HTMLDivElement | null>(null)
|
||||
const rememberedSelection = useRef<BaseSelection | null>(null)
|
||||
@ -38,12 +35,11 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => {
|
||||
|
||||
const textEditorRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const closeEditor = () => onClose(convertValueToStepContent(value))
|
||||
|
||||
useOutsideClick({
|
||||
ref: textEditorRef,
|
||||
handler: () => {
|
||||
save(value)
|
||||
onClose()
|
||||
},
|
||||
handler: closeEditor,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@ -69,18 +65,16 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const save = (value: unknown[]) => {
|
||||
if (value.length === 0) return
|
||||
const convertValueToStepContent = (value: unknown[]): TextBubbleContent => {
|
||||
if (value.length === 0) defaultTextBubbleContent
|
||||
const html = serializeHtml(editor, {
|
||||
nodes: value,
|
||||
})
|
||||
updateStep(stepId, {
|
||||
content: {
|
||||
html,
|
||||
richText: value,
|
||||
plainText: parseHtmlStringToPlainText(html),
|
||||
},
|
||||
} as TextBubbleStep)
|
||||
return {
|
||||
html,
|
||||
richText: value,
|
||||
plainText: parseHtmlStringToPlainText(html),
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
@ -99,6 +93,11 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => {
|
||||
setValue(val)
|
||||
setIsVariableDropdownOpen(false)
|
||||
}
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.shiftKey) return
|
||||
if (e.key === 'Enter') closeEditor()
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
flex="1"
|
||||
@ -126,6 +125,7 @@ export const TextBubbleEditor = ({ initialValue, stepId, onClose }: Props) => {
|
||||
onBlur: () => {
|
||||
rememberedSelection.current = editor.selection
|
||||
},
|
||||
onKeyDown: handleKeyDown,
|
||||
}}
|
||||
initialValue={
|
||||
initialValue.length === 0
|
||||
|
@ -2,20 +2,20 @@ import { Box, Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react'
|
||||
import { TrashIcon, PlusIcon } from 'assets/icons'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { Draft } from 'immer'
|
||||
import { Table } from 'models'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { generate } from 'short-uuid'
|
||||
import { useImmer } from 'use-immer'
|
||||
|
||||
type ItemWithId<T> = T & { id: string }
|
||||
|
||||
export type TableListItemProps<T> = {
|
||||
id: string
|
||||
item: T
|
||||
onItemChange: (item: T) => void
|
||||
}
|
||||
|
||||
type Props<T> = {
|
||||
initialItems: Table<T>
|
||||
onItemsChange: (items: Table<T>) => void
|
||||
initialItems: ItemWithId<T>[]
|
||||
onItemsChange: (items: ItemWithId<T>[]) => void
|
||||
addLabel?: string
|
||||
Item: (props: TableListItemProps<T>) => JSX.Element
|
||||
ComponentBetweenItems?: (props: unknown) => JSX.Element
|
||||
@ -29,7 +29,7 @@ export const TableList = <T,>({
|
||||
ComponentBetweenItems = () => <></>,
|
||||
}: Props<T>) => {
|
||||
const [items, setItems] = useImmer(initialItems)
|
||||
const [showDeleteId, setShowDeleteId] = useState<string | undefined>()
|
||||
const [showDeleteIndex, setShowDeleteIndex] = useState<number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (deepEqual(items, initialItems)) return
|
||||
@ -40,55 +40,47 @@ export const TableList = <T,>({
|
||||
const createItem = () => {
|
||||
setItems((items) => {
|
||||
const id = generate()
|
||||
items.byId[id] = { id } as unknown as Draft<T>
|
||||
items.allIds.push(id)
|
||||
const newItem = { id } as Draft<ItemWithId<T>>
|
||||
items.push(newItem)
|
||||
})
|
||||
}
|
||||
|
||||
const updateItem = (itemId: string, updates: Partial<T>) =>
|
||||
const updateItem = (itemIndex: number, updates: Partial<T>) =>
|
||||
setItems((items) => {
|
||||
items.byId[itemId] = {
|
||||
...items.byId[itemId],
|
||||
...updates,
|
||||
}
|
||||
items[itemIndex] = { ...items[itemIndex], ...updates }
|
||||
})
|
||||
|
||||
const deleteItem = (itemId: string) => () => {
|
||||
const deleteItem = (itemIndex: number) => () => {
|
||||
setItems((items) => {
|
||||
delete items.byId[itemId]
|
||||
const index = items.allIds.indexOf(itemId)
|
||||
if (index !== -1) items.allIds.splice(index, 1)
|
||||
items.splice(itemIndex, 1)
|
||||
})
|
||||
}
|
||||
|
||||
const handleMouseEnter = (itemId: string) => () => setShowDeleteId(itemId)
|
||||
const handleMouseEnter = (itemIndex: number) => () =>
|
||||
setShowDeleteIndex(itemIndex)
|
||||
|
||||
const handleCellChange = (itemId: string) => (item: T) =>
|
||||
updateItem(itemId, item)
|
||||
const handleCellChange = (itemIndex: number) => (item: T) =>
|
||||
updateItem(itemIndex, item)
|
||||
|
||||
const handleMouseLeave = () => setShowDeleteId(undefined)
|
||||
const handleMouseLeave = () => setShowDeleteIndex(null)
|
||||
|
||||
return (
|
||||
<Stack spacing="4">
|
||||
{items.allIds.map((itemId, idx) => (
|
||||
<Box key={itemId}>
|
||||
{idx !== 0 && <ComponentBetweenItems />}
|
||||
{items.map((item, itemIndex) => (
|
||||
<Box key={item.id}>
|
||||
{itemIndex !== 0 && <ComponentBetweenItems />}
|
||||
<Flex
|
||||
pos="relative"
|
||||
onMouseEnter={handleMouseEnter(itemId)}
|
||||
onMouseEnter={handleMouseEnter(itemIndex)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
mt={idx !== 0 && ComponentBetweenItems ? 4 : 0}
|
||||
mt={itemIndex !== 0 && ComponentBetweenItems ? 4 : 0}
|
||||
>
|
||||
<Item
|
||||
id={itemId}
|
||||
item={items.byId[itemId]}
|
||||
onItemChange={handleCellChange(itemId)}
|
||||
/>
|
||||
<Fade in={showDeleteId === itemId}>
|
||||
<Item item={item} onItemChange={handleCellChange(itemIndex)} />
|
||||
<Fade in={showDeleteIndex === itemIndex}>
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove cell"
|
||||
onClick={deleteItem(itemId)}
|
||||
onClick={deleteItem(itemIndex)}
|
||||
pos="absolute"
|
||||
left="-15px"
|
||||
top="-15px"
|
||||
|
@ -13,10 +13,10 @@ import {
|
||||
import { PlusIcon, TrashIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { Variable } from 'models'
|
||||
import React, { useState, useRef, ChangeEvent, useMemo, useEffect } from 'react'
|
||||
import React, { useState, useRef, ChangeEvent, useEffect } from 'react'
|
||||
import { generate } from 'short-uuid'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { isNotDefined } from 'utils'
|
||||
import { byId, isNotDefined } from 'utils'
|
||||
|
||||
type Props = {
|
||||
initialVariableId?: string
|
||||
@ -34,16 +34,14 @@ export const VariableSearchInput = ({
|
||||
}: Props) => {
|
||||
const { onOpen, onClose, isOpen } = useDisclosure()
|
||||
const { typebot, createVariable, deleteVariable } = useTypebot()
|
||||
const variables = useMemo(
|
||||
() =>
|
||||
typebot?.variables.allIds.map((id) => typebot.variables.byId[id]) ?? [],
|
||||
[typebot?.variables]
|
||||
)
|
||||
const variables = typebot?.variables ?? []
|
||||
const [inputValue, setInputValue] = useState(
|
||||
typebot?.variables.byId[initialVariableId ?? '']?.name ?? ''
|
||||
variables.find(byId(initialVariableId))?.name ?? ''
|
||||
)
|
||||
const [debouncedInputValue] = useDebounce(inputValue, 200)
|
||||
const [filteredItems, setFilteredItems] = useState<Variable[]>(variables)
|
||||
const [filteredItems, setFilteredItems] = useState<Variable[]>(
|
||||
variables ?? []
|
||||
)
|
||||
const dropdownRef = useRef(null)
|
||||
const inputRef = useRef(null)
|
||||
|
||||
|
@ -15,7 +15,7 @@ export const ButtonsTheme = ({ buttons, onButtonsChange }: Props) => {
|
||||
onButtonsChange({ ...buttons, color })
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack data-testid="buttons-theme">
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Background:</Text>
|
||||
<ColorPicker
|
||||
|
@ -15,7 +15,7 @@ export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
|
||||
onGuestBubblesChange({ ...guestBubbles, color })
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack data-testid="guest-bubbles-theme">
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Background:</Text>
|
||||
<ColorPicker
|
||||
|
@ -15,7 +15,7 @@ export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
|
||||
onHostBubblesChange({ ...hostBubbles, color })
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack data-testid="host-bubbles-theme">
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Background:</Text>
|
||||
<ColorPicker
|
||||
|
@ -17,7 +17,7 @@ export const InputsTheme = ({ inputs, onInputsChange }: Props) => {
|
||||
onInputsChange({ ...inputs, placeholderColor })
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack data-testid="inputs-theme">
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Background:</Text>
|
||||
<ColorPicker
|
||||
|
@ -41,6 +41,8 @@ export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
|
||||
const handleColorChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
setColor(e.target.value)
|
||||
|
||||
const handleClick = (color: string) => () => setColor(color)
|
||||
|
||||
return (
|
||||
<Popover variant="picker" placement="right" isLazy>
|
||||
<PopoverTrigger>
|
||||
@ -79,10 +81,8 @@ export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
|
||||
minWidth="unset"
|
||||
borderRadius={3}
|
||||
_hover={{ background: c }}
|
||||
onClick={() => {
|
||||
setColor(c)
|
||||
}}
|
||||
></Button>
|
||||
onClick={handleClick(c)}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
<Input
|
||||
|
Reference in New Issue
Block a user