feat(engine): ✨ Link typebot step
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { IconProps } from '@chakra-ui/react'
|
||||
import {
|
||||
BoxIcon,
|
||||
CalendarIcon,
|
||||
ChatIcon,
|
||||
CheckSquareIcon,
|
||||
@@ -60,6 +61,8 @@ export const StepIcon = ({ type, ...props }: StepIconProps) => {
|
||||
return <ExternalLinkIcon color="purple.500" {...props} />
|
||||
case LogicStepType.CODE:
|
||||
return <CodeIcon color="purple.500" {...props} />
|
||||
case LogicStepType.TYPEBOT_LINK:
|
||||
return <BoxIcon color="purple.500" {...props} />
|
||||
case IntegrationStepType.GOOGLE_SHEETS:
|
||||
return <GoogleSheetsLogo {...props} />
|
||||
case IntegrationStepType.GOOGLE_ANALYTICS:
|
||||
|
||||
@@ -43,6 +43,12 @@ export const StepTypeLabel = ({ type }: Props) => {
|
||||
<Text>Code</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
case LogicStepType.TYPEBOT_LINK:
|
||||
return (
|
||||
<Tooltip label="Link to another of your typebots">
|
||||
<Text>Typebot</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
case IntegrationStepType.GOOGLE_SHEETS:
|
||||
return (
|
||||
<Tooltip label="Google Sheets">
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
/* eslint-disable react/jsx-key */
|
||||
import { Button, chakra, Checkbox, Flex, HStack, Text } from '@chakra-ui/react'
|
||||
import { AlignLeftTextIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { PublicTypebot } from 'models'
|
||||
import React, { useEffect, useMemo, useRef } from 'react'
|
||||
import { Hooks, useRowSelect, useTable } from 'react-table'
|
||||
import { parseSubmissionsColumns } from 'services/publicTypebot'
|
||||
import { LoadingRows } from './LoadingRows'
|
||||
|
||||
type SubmissionsTableProps = {
|
||||
blocksAndVariables: Pick<PublicTypebot, 'blocks' | 'variables'>
|
||||
data?: any
|
||||
hasMore?: boolean
|
||||
onNewSelection: (indices: number[]) => void
|
||||
@@ -17,16 +18,16 @@ type SubmissionsTableProps = {
|
||||
}
|
||||
|
||||
export const SubmissionsTable = ({
|
||||
blocksAndVariables,
|
||||
data,
|
||||
hasMore,
|
||||
onNewSelection,
|
||||
onScrollToBottom,
|
||||
onLogOpenIndex,
|
||||
}: SubmissionsTableProps) => {
|
||||
const { publishedTypebot } = useTypebot()
|
||||
const columns: any = useMemo(
|
||||
() => (publishedTypebot ? parseSubmissionsColumns(publishedTypebot) : []),
|
||||
[publishedTypebot]
|
||||
() => parseSubmissionsColumns(blocksAndVariables),
|
||||
[blocksAndVariables]
|
||||
)
|
||||
const bottomElement = useRef<HTMLDivElement | null>(null)
|
||||
const tableWrapper = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { css } from '@codemirror/lang-css'
|
||||
import { javascript } from '@codemirror/lang-javascript'
|
||||
import { html } from '@codemirror/lang-html'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
@@ -22,6 +23,10 @@ export const CodeEditor = ({
|
||||
const editorContainer = useRef<HTMLDivElement | null>(null)
|
||||
const editorView = useRef<EditorView | null>(null)
|
||||
const [plainTextValue, setPlainTextValue] = useState(value)
|
||||
const [debouncedValue] = useDebounce(
|
||||
plainTextValue,
|
||||
process.env.NEXT_PUBLIC_E2E_TEST ? 0 : 1000
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorView.current || !isReadOnly) return
|
||||
@@ -36,10 +41,10 @@ export const CodeEditor = ({
|
||||
}, [value])
|
||||
|
||||
useEffect(() => {
|
||||
if (!onChange || plainTextValue === value) return
|
||||
onChange(plainTextValue)
|
||||
if (!onChange || debouncedValue === value) return
|
||||
onChange(debouncedValue)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [plainTextValue])
|
||||
}, [debouncedValue])
|
||||
|
||||
useEffect(() => {
|
||||
const editor = initEditor(value)
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
|
||||
type Props = Omit<InputProps, 'onChange' | 'value'> & {
|
||||
initialValue: string
|
||||
@@ -18,16 +19,19 @@ export const DebouncedInput = forwardRef(
|
||||
ref: ForwardedRef<HTMLInputElement>
|
||||
) => {
|
||||
const [currentValue, setCurrentValue] = useState(initialValue)
|
||||
const [debouncedValue] = useDebounce(
|
||||
currentValue,
|
||||
process.env.NEXT_PUBLIC_E2E_TEST ? 0 : 1000
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (currentValue === initialValue) return
|
||||
onChange(currentValue)
|
||||
if (debouncedValue === initialValue) return
|
||||
onChange(debouncedValue)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentValue])
|
||||
}, [debouncedValue])
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
setCurrentValue(e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Textarea, TextareaProps } from '@chakra-ui/react'
|
||||
import { ChangeEvent, useEffect, useState } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
|
||||
type Props = Omit<TextareaProps, 'onChange' | 'value'> & {
|
||||
initialValue: string
|
||||
@@ -12,12 +13,16 @@ export const DebouncedTextarea = ({
|
||||
...props
|
||||
}: Props) => {
|
||||
const [currentValue, setCurrentValue] = useState(initialValue)
|
||||
const [debouncedValue] = useDebounce(
|
||||
currentValue,
|
||||
process.env.NEXT_PUBLIC_E2E_TEST ? 0 : 1000
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (currentValue === initialValue) return
|
||||
onChange(currentValue)
|
||||
if (debouncedValue === initialValue) return
|
||||
onChange(debouncedValue)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentValue])
|
||||
}, [debouncedValue])
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setCurrentValue(e.target.value)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Coordinates, useGraph } from 'contexts/GraphContext'
|
||||
import React, { useLayoutEffect, useMemo, useState } from 'react'
|
||||
import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
getAnchorsPosition,
|
||||
computeEdgePath,
|
||||
@@ -31,6 +31,7 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
|
||||
const [isMouseOver, setIsMouseOver] = useState(false)
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const [edgeMenuPosition, setEdgeMenuPosition] = useState({ x: 0, y: 0 })
|
||||
const [refreshEdge, setRefreshEdge] = useState(false)
|
||||
|
||||
const isPreviewing = isMouseOver || previewingEdge?.id === edge.id
|
||||
|
||||
@@ -47,9 +48,13 @@ export const Edge = ({ edge }: { edge: EdgeProps }) => {
|
||||
getSourceEndpointId(edge)
|
||||
),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[sourceBlockCoordinates?.y, edge, sourceEndpoints]
|
||||
[sourceBlockCoordinates?.y, edge, sourceEndpoints, refreshEdge]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => setRefreshEdge(true), 50)
|
||||
}, [])
|
||||
|
||||
const [targetTop, setTargetTop] = useState(
|
||||
getEndpointTopOffset(targetEndpoints, graphPosition.y, edge?.to.stepId)
|
||||
)
|
||||
|
||||
@@ -33,8 +33,9 @@ import { GoogleAnalyticsSettings } from './bodies/GoogleAnalyticsSettings'
|
||||
import { GoogleSheetsSettingsBody } from './bodies/GoogleSheetsSettingsBody'
|
||||
import { PhoneNumberSettingsBody } from './bodies/PhoneNumberSettingsBody'
|
||||
import { RedirectSettings } from './bodies/RedirectSettings'
|
||||
import { SendEmailSettings } from './bodies/SendEmailSettings/SendEmailSettings'
|
||||
import { SendEmailSettings } from './bodies/SendEmailSettings'
|
||||
import { SetVariableSettings } from './bodies/SetVariableSettings'
|
||||
import { TypebotLinkSettingsForm } from './bodies/TypebotLinkSettingsForm'
|
||||
import { WebhookSettings } from './bodies/WebhookSettings'
|
||||
import { ZapierSettings } from './bodies/ZapierSettings'
|
||||
|
||||
@@ -43,7 +44,6 @@ type Props = {
|
||||
webhook?: Webhook
|
||||
onExpandClick: () => void
|
||||
onStepChange: (updates: Partial<Step>) => void
|
||||
onTestRequestClick: () => void
|
||||
}
|
||||
|
||||
export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
|
||||
@@ -85,12 +85,10 @@ export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
|
||||
export const StepSettings = ({
|
||||
step,
|
||||
onStepChange,
|
||||
onTestRequestClick,
|
||||
}: {
|
||||
step: Step
|
||||
webhook?: Webhook
|
||||
onStepChange: (step: Partial<Step>) => void
|
||||
onTestRequestClick: () => void
|
||||
}) => {
|
||||
const handleOptionsChange = (options: StepOptions) => {
|
||||
onStepChange({ options } as Partial<Step>)
|
||||
@@ -186,6 +184,14 @@ export const StepSettings = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
case LogicStepType.TYPEBOT_LINK: {
|
||||
return (
|
||||
<TypebotLinkSettingsForm
|
||||
options={step.options}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case IntegrationStepType.GOOGLE_SHEETS: {
|
||||
return (
|
||||
<GoogleSheetsSettingsBody
|
||||
@@ -208,11 +214,7 @@ export const StepSettings = ({
|
||||
}
|
||||
case IntegrationStepType.WEBHOOK: {
|
||||
return (
|
||||
<WebhookSettings
|
||||
step={step}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onTestRequestClick={onTestRequestClick}
|
||||
/>
|
||||
<WebhookSettings step={step} onOptionsChange={handleOptionsChange} />
|
||||
)
|
||||
}
|
||||
case IntegrationStepType.EMAIL: {
|
||||
|
||||
@@ -25,8 +25,9 @@ export const NumberInputSettingsBody = ({
|
||||
onOptionsChange(removeUndefinedFields({ ...options, max }))
|
||||
const handleStepChange = (step?: number) =>
|
||||
onOptionsChange(removeUndefinedFields({ ...options, step }))
|
||||
const handleVariableChange = (variable?: Variable) =>
|
||||
const handleVariableChange = (variable?: Variable) => {
|
||||
onOptionsChange({ ...options, variableId: variable?.id })
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Input } from '@chakra-ui/react'
|
||||
import { SearchableDropdown } from 'components/shared/SearchableDropdown'
|
||||
import { Block } from 'models'
|
||||
import { useMemo } from 'react'
|
||||
import { byId } from 'utils'
|
||||
|
||||
type Props = {
|
||||
blocks: Block[]
|
||||
blockId?: string
|
||||
onBlockIdSelected: (blockId: string) => void
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
export const BlocksDropdown = ({
|
||||
blocks,
|
||||
blockId,
|
||||
onBlockIdSelected,
|
||||
isLoading,
|
||||
}: Props) => {
|
||||
const currentBlock = useMemo(
|
||||
() => blocks?.find(byId(blockId)),
|
||||
[blockId, blocks]
|
||||
)
|
||||
|
||||
const handleBlockSelect = (title: string) => {
|
||||
const id = blocks?.find((b) => b.title === title)?.id
|
||||
if (id) onBlockIdSelected(id)
|
||||
}
|
||||
|
||||
if (isLoading) return <Input value="Loading..." isDisabled />
|
||||
if (!blocks || blocks.length === 0)
|
||||
return <Input value="No blocks found" isDisabled />
|
||||
return (
|
||||
<SearchableDropdown
|
||||
selectedItem={currentBlock?.title}
|
||||
items={(blocks ?? []).map((b) => b.title)}
|
||||
onValueChange={handleBlockSelect}
|
||||
placeholder={'Select a block'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Stack } from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { TypebotLinkOptions } from 'models'
|
||||
import { byId } from 'utils'
|
||||
import { BlocksDropdown } from './BlocksDropdown'
|
||||
import { TypebotsDropdown } from './TypebotsDropdown'
|
||||
|
||||
type Props = {
|
||||
options: TypebotLinkOptions
|
||||
onOptionsChange: (options: TypebotLinkOptions) => void
|
||||
}
|
||||
|
||||
export const TypebotLinkSettingsForm = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
}: Props) => {
|
||||
const { linkedTypebots, typebot } = useTypebot()
|
||||
|
||||
const handleTypebotIdChange = (typebotId: string) =>
|
||||
onOptionsChange({ ...options, typebotId })
|
||||
const handleBlockIdChange = (blockId: string) =>
|
||||
onOptionsChange({ ...options, blockId })
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<TypebotsDropdown
|
||||
typebotId={options.typebotId}
|
||||
onSelectTypebotId={handleTypebotIdChange}
|
||||
/>
|
||||
<BlocksDropdown
|
||||
blocks={
|
||||
typebot && options.typebotId === typebot.id
|
||||
? typebot.blocks
|
||||
: linkedTypebots?.find(byId(options.typebotId))?.blocks ?? []
|
||||
}
|
||||
blockId={options.blockId}
|
||||
onBlockIdSelected={handleBlockIdChange}
|
||||
isLoading={
|
||||
linkedTypebots === undefined &&
|
||||
typebot &&
|
||||
typebot.id !== options.typebotId
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { HStack, IconButton, Input, useToast } from '@chakra-ui/react'
|
||||
import { ExternalLinkIcon } from 'assets/icons'
|
||||
import { NextChakraLink } from 'components/nextChakra/NextChakraLink'
|
||||
import { SearchableDropdown } from 'components/shared/SearchableDropdown'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMemo } from 'react'
|
||||
import { useTypebots } from 'services/typebots'
|
||||
import { byId } from 'utils'
|
||||
|
||||
type Props = {
|
||||
typebotId?: string
|
||||
onSelectTypebotId: (typebotId: string) => void
|
||||
}
|
||||
|
||||
export const TypebotsDropdown = ({ typebotId, onSelectTypebotId }: Props) => {
|
||||
const { query } = useRouter()
|
||||
const toast = useToast({
|
||||
position: 'top-right',
|
||||
status: 'error',
|
||||
})
|
||||
const { typebots, isLoading } = useTypebots({
|
||||
allFolders: true,
|
||||
onError: (e) => toast({ title: e.name, description: e.message }),
|
||||
})
|
||||
const currentTypebot = useMemo(
|
||||
() => typebots?.find(byId(typebotId)),
|
||||
[typebotId, typebots]
|
||||
)
|
||||
|
||||
const handleTypebotSelect = (name: string) => {
|
||||
const id = typebots?.find((s) => s.name === name)?.id
|
||||
if (id) onSelectTypebotId(id)
|
||||
}
|
||||
|
||||
if (isLoading) return <Input value="Loading..." isDisabled />
|
||||
if (!typebots || typebots.length === 0)
|
||||
return <Input value="No typebots found" isDisabled />
|
||||
return (
|
||||
<HStack>
|
||||
<SearchableDropdown
|
||||
selectedItem={currentTypebot?.name}
|
||||
items={(typebots ?? []).map((t) => t.name)}
|
||||
onValueChange={handleTypebotSelect}
|
||||
placeholder={'Select a typebot'}
|
||||
/>
|
||||
{currentTypebot?.id && (
|
||||
<IconButton
|
||||
aria-label="Navigate to typebot"
|
||||
icon={<ExternalLinkIcon />}
|
||||
as={NextChakraLink}
|
||||
href={`/typebots/${currentTypebot?.id}/edit?parentId=${query.typebotId}`}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { TypebotLinkSettingsForm } from './TypebotLinkSettingsForm'
|
||||
@@ -42,13 +42,11 @@ import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||
type Props = {
|
||||
step: WebhookStep
|
||||
onOptionsChange: (options: WebhookOptions) => void
|
||||
onTestRequestClick: () => void
|
||||
}
|
||||
|
||||
export const WebhookSettings = ({
|
||||
step: { options, blockId, id: stepId, webhookId },
|
||||
onOptionsChange,
|
||||
onTestRequestClick,
|
||||
}: Props) => {
|
||||
const { typebot, save, webhooks, updateWebhook } = useTypebot()
|
||||
const [isTestResponseLoading, setIsTestResponseLoading] = useState(false)
|
||||
@@ -122,7 +120,6 @@ export const WebhookSettings = ({
|
||||
const handleTestRequestClick = async () => {
|
||||
if (!typebot || !localWebhook) return
|
||||
setIsTestResponseLoading(true)
|
||||
onTestRequestClick()
|
||||
await Promise.all([updateWebhook(localWebhook.id, localWebhook), save()])
|
||||
const { data, error } = await executeWebhook(
|
||||
typebot.id,
|
||||
|
||||
@@ -48,7 +48,6 @@ export const StepNode = ({
|
||||
const { setConnectingIds, connectingIds, openedStepId, setOpenedStepId } =
|
||||
useGraph()
|
||||
const { updateStep } = useTypebot()
|
||||
const [localStep, setLocalStep] = useState(step)
|
||||
const [isConnecting, setIsConnecting] = useState(false)
|
||||
const [isPopoverOpened, setIsPopoverOpened] = useState(
|
||||
openedStepId === step.id
|
||||
@@ -74,10 +73,6 @@ export const StepNode = ({
|
||||
onClose: onModalClose,
|
||||
} = useDisclosure()
|
||||
|
||||
useEffect(() => {
|
||||
setLocalStep(step)
|
||||
}, [step])
|
||||
|
||||
useEffect(() => {
|
||||
if (query.stepId?.toString() === step.id) setOpenedStepId(step.id)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -91,7 +86,7 @@ export const StepNode = ({
|
||||
}, [connectingIds, step.blockId, step.id])
|
||||
|
||||
const handleModalClose = () => {
|
||||
updateStep(indices, { ...localStep })
|
||||
updateStep(indices, { ...step })
|
||||
onModalClose()
|
||||
}
|
||||
|
||||
@@ -112,8 +107,7 @@ export const StepNode = ({
|
||||
}
|
||||
|
||||
const handleCloseEditor = (content: TextBubbleContent) => {
|
||||
const updatedStep = { ...localStep, content } as Step
|
||||
setLocalStep(updatedStep)
|
||||
const updatedStep = { ...step, content } as Step
|
||||
updateStep(indices, updatedStep)
|
||||
setIsEditing(false)
|
||||
}
|
||||
@@ -129,26 +123,20 @@ export const StepNode = ({
|
||||
onModalOpen()
|
||||
}
|
||||
|
||||
const updateOptions = () => {
|
||||
updateStep(indices, { ...localStep })
|
||||
}
|
||||
|
||||
const handleStepChange = (updates: Partial<Step>) => {
|
||||
setLocalStep({ ...localStep, ...updates } as Step)
|
||||
}
|
||||
const handleStepUpdate = (updates: Partial<Step>) =>
|
||||
updateStep(indices, { ...step, ...updates })
|
||||
|
||||
const handleContentChange = (content: BubbleStepContent) =>
|
||||
setLocalStep({ ...localStep, content } as Step)
|
||||
updateStep(indices, { ...step, content } as Step)
|
||||
|
||||
useEffect(() => {
|
||||
if (isPopoverOpened && openedStepId !== step.id) updateOptions()
|
||||
setIsPopoverOpened(openedStepId === step.id)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [openedStepId])
|
||||
|
||||
return isEditing && isTextBubbleStep(localStep) ? (
|
||||
return isEditing && isTextBubbleStep(step) ? (
|
||||
<TextBubbleEditor
|
||||
initialValue={localStep.content.richText}
|
||||
initialValue={step.content.richText}
|
||||
onClose={handleCloseEditor}
|
||||
/>
|
||||
) : (
|
||||
@@ -185,22 +173,22 @@ export const StepNode = ({
|
||||
w="full"
|
||||
>
|
||||
<StepIcon
|
||||
type={localStep.type}
|
||||
type={step.type}
|
||||
mt="1"
|
||||
data-testid={`${localStep.id}-icon`}
|
||||
data-testid={`${step.id}-icon`}
|
||||
/>
|
||||
<StepNodeContent step={localStep} indices={indices} />
|
||||
<StepNodeContent step={step} indices={indices} />
|
||||
<TargetEndpoint
|
||||
pos="absolute"
|
||||
left="-32px"
|
||||
top="19px"
|
||||
stepId={localStep.id}
|
||||
stepId={step.id}
|
||||
/>
|
||||
{isConnectable && hasDefaultConnector(localStep) && (
|
||||
{isConnectable && hasDefaultConnector(step) && (
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
blockId: localStep.blockId,
|
||||
stepId: localStep.id,
|
||||
blockId: step.blockId,
|
||||
stepId: step.id,
|
||||
}}
|
||||
pos="absolute"
|
||||
right="15px"
|
||||
@@ -210,26 +198,21 @@ export const StepNode = ({
|
||||
</HStack>
|
||||
</Flex>
|
||||
</PopoverTrigger>
|
||||
{hasSettingsPopover(localStep) && (
|
||||
{hasSettingsPopover(step) && (
|
||||
<SettingsPopoverContent
|
||||
step={localStep}
|
||||
step={step}
|
||||
onExpandClick={handleExpandClick}
|
||||
onStepChange={handleStepChange}
|
||||
onTestRequestClick={updateOptions}
|
||||
onStepChange={handleStepUpdate}
|
||||
/>
|
||||
)}
|
||||
{isMediaBubbleStep(localStep) && (
|
||||
{isMediaBubbleStep(step) && (
|
||||
<MediaBubblePopoverContent
|
||||
step={localStep}
|
||||
step={step}
|
||||
onContentChange={handleContentChange}
|
||||
/>
|
||||
)}
|
||||
<SettingsModal isOpen={isModalOpen} onClose={handleModalClose}>
|
||||
<StepSettings
|
||||
step={localStep}
|
||||
onStepChange={handleStepChange}
|
||||
onTestRequestClick={updateOptions}
|
||||
/>
|
||||
<StepSettings step={step} onStepChange={handleStepUpdate} />
|
||||
</SettingsModal>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { ConfigureContent } from './contents/ConfigureContent'
|
||||
import { ImageBubbleContent } from './contents/ImageBubbleContent'
|
||||
import { PlaceholderContent } from './contents/PlaceholderContent'
|
||||
import { SendEmailContent } from './contents/SendEmailContent'
|
||||
import { TypebotLinkContent } from './contents/TypebotLinkContent'
|
||||
import { ZapierContent } from './contents/ZapierContent'
|
||||
|
||||
type Props = {
|
||||
@@ -87,6 +88,8 @@ export const StepNodeContent = ({ step, indices }: Props) => {
|
||||
/>
|
||||
)
|
||||
}
|
||||
case LogicStepType.TYPEBOT_LINK:
|
||||
return <TypebotLinkContent step={step} />
|
||||
|
||||
case IntegrationStepType.GOOGLE_SHEETS: {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { TypebotLinkStep } from 'models'
|
||||
import React from 'react'
|
||||
import { Tag, Text } from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { byId } from 'utils'
|
||||
|
||||
type Props = {
|
||||
step: TypebotLinkStep
|
||||
}
|
||||
|
||||
export const TypebotLinkContent = ({ step }: Props) => {
|
||||
const { linkedTypebots, typebot } = useTypebot()
|
||||
const isCurrentTypebot = typebot && step.options.typebotId === typebot.id
|
||||
const linkedTypebot = isCurrentTypebot
|
||||
? typebot
|
||||
: linkedTypebots?.find(byId(step.options.typebotId))
|
||||
const blockTitle = linkedTypebot?.blocks.find(
|
||||
byId(step.options.blockId)
|
||||
)?.title
|
||||
if (!step.options.typebotId) return <Text color="gray.500">Configure...</Text>
|
||||
return (
|
||||
<Text>
|
||||
Jump{' '}
|
||||
{blockTitle ? (
|
||||
<>
|
||||
to <Tag>{blockTitle}</Tag>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}{' '}
|
||||
{!isCurrentTypebot ? (
|
||||
<>
|
||||
in <Tag colorScheme="blue">{linkedTypebot?.name}</Tag>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@@ -26,7 +26,10 @@ export const SearchableDropdown = ({
|
||||
}: Props) => {
|
||||
const { onOpen, onClose, isOpen } = useDisclosure()
|
||||
const [inputValue, setInputValue] = useState(selectedItem ?? '')
|
||||
const [debouncedInputValue] = useDebounce(inputValue, 200)
|
||||
const [debouncedInputValue] = useDebounce(
|
||||
inputValue,
|
||||
process.env.NEXT_PUBLIC_E2E_TEST ? 0 : 1000
|
||||
)
|
||||
const [filteredItems, setFilteredItems] = useState([
|
||||
...items
|
||||
.filter((item) =>
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
} from '@chakra-ui/react'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
|
||||
export const SmartNumberInput = ({
|
||||
value,
|
||||
@@ -17,14 +18,26 @@ export const SmartNumberInput = ({
|
||||
onValueChange: (value?: number) => void
|
||||
} & NumberInputProps) => {
|
||||
const [currentValue, setCurrentValue] = useState(value?.toString() ?? '')
|
||||
const [valueToReturn, setValueToReturn] = useState<number | undefined>(
|
||||
parseFloat(currentValue)
|
||||
)
|
||||
const [debouncedValue] = useDebounce(
|
||||
valueToReturn,
|
||||
process.env.NEXT_PUBLIC_E2E_TEST ? 0 : 1000
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
onValueChange(debouncedValue)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedValue])
|
||||
|
||||
const handleValueChange = (value: string) => {
|
||||
setCurrentValue(value)
|
||||
if (value.endsWith('.') || value.endsWith(',')) return
|
||||
if (value === '') return onValueChange(undefined)
|
||||
if (value === '') return setValueToReturn(undefined)
|
||||
const newValue = parseFloat(value)
|
||||
if (isNaN(newValue)) return
|
||||
onValueChange(newValue)
|
||||
setValueToReturn(newValue)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { UserIcon } from 'assets/icons'
|
||||
import { Variable } from 'models'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { VariableSearchInput } from '../VariableSearchInput'
|
||||
|
||||
export type TextBoxWithVariableButtonProps = {
|
||||
@@ -33,12 +34,16 @@ export const TextBoxWithVariableButton = ({
|
||||
null
|
||||
)
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [debouncedValue] = useDebounce(
|
||||
value,
|
||||
process.env.NEXT_PUBLIC_E2E_TEST ? 0 : 1000
|
||||
)
|
||||
const [carretPosition, setCarretPosition] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
onChange(value)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value])
|
||||
}, [debouncedValue])
|
||||
|
||||
const handleVariableSelected = (variable?: Variable) => {
|
||||
if (!textBoxRef.current || !variable) return
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { Editable, EditablePreview, EditableInput } from '@chakra-ui/editable'
|
||||
import { Tooltip } from '@chakra-ui/tooltip'
|
||||
import React from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
type EditableProps = {
|
||||
name: string
|
||||
onNewName: (newName: string) => void
|
||||
}
|
||||
export const EditableTypebotName = ({ name, onNewName }: EditableProps) => {
|
||||
const [localName, setLocalName] = useState(name)
|
||||
|
||||
useEffect(() => {
|
||||
if (name !== localName) setLocalName(name)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [name])
|
||||
|
||||
return (
|
||||
<Tooltip label="Rename">
|
||||
<Editable defaultValue={name} onSubmit={onNewName}>
|
||||
<Editable value={localName} onChange={setLocalName} onSubmit={onNewName}>
|
||||
<EditablePreview
|
||||
isTruncated
|
||||
cursor="pointer"
|
||||
|
||||
@@ -106,10 +106,12 @@ export const TypebotHeader = () => {
|
||||
<HStack alignItems="center">
|
||||
<IconButton
|
||||
as={NextChakraLink}
|
||||
aria-label="Back"
|
||||
aria-label="Navigate back"
|
||||
icon={<ChevronLeftIcon fontSize={30} />}
|
||||
href={
|
||||
typebot?.folderId
|
||||
router.query.parentId
|
||||
? `/typebots/${router.query.parentId}/edit`
|
||||
: typebot?.folderId
|
||||
? `/typebots/folders/${typebot.folderId}`
|
||||
: '/typebots'
|
||||
}
|
||||
|
||||
@@ -38,7 +38,10 @@ export const VariableSearchInput = ({
|
||||
const [inputValue, setInputValue] = useState(
|
||||
variables.find(byId(initialVariableId))?.name ?? ''
|
||||
)
|
||||
const [debouncedInputValue] = useDebounce(inputValue, 200)
|
||||
const [debouncedInputValue] = useDebounce(
|
||||
inputValue,
|
||||
process.env.NEXT_PUBLIC_E2E_TEST ? 0 : 1000
|
||||
)
|
||||
const [filteredItems, setFilteredItems] = useState<Variable[]>(
|
||||
variables ?? []
|
||||
)
|
||||
@@ -57,7 +60,7 @@ export const VariableSearchInput = ({
|
||||
|
||||
useEffect(() => {
|
||||
const variable = variables.find((v) => v.name === debouncedInputValue)
|
||||
if (variable) onSelectVariable(variable)
|
||||
onSelectVariable(variable)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedInputValue])
|
||||
|
||||
@@ -66,7 +69,6 @@ export const VariableSearchInput = ({
|
||||
onOpen()
|
||||
if (e.target.value === '') {
|
||||
setFilteredItems([...variables.slice(0, 50)])
|
||||
onSelectVariable(undefined)
|
||||
return
|
||||
}
|
||||
setFilteredItems([
|
||||
@@ -80,15 +82,14 @@ export const VariableSearchInput = ({
|
||||
|
||||
const handleVariableNameClick = (variable: Variable) => () => {
|
||||
setInputValue(variable.name)
|
||||
onSelectVariable(variable)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleCreateNewVariableClick = () => {
|
||||
if (!inputValue || inputValue === '') return
|
||||
const id = generate()
|
||||
createVariable({ id, name: inputValue })
|
||||
onSelectVariable({ id, name: inputValue })
|
||||
createVariable({ id, name: inputValue })
|
||||
onClose()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user