feat(engine): ✨ Link typebot step
This commit is contained in:
@ -371,3 +371,11 @@ export const AlignLeftTextIcon = (props: IconProps) => (
|
||||
<line x1="17" y1="18" x2="3" y2="18"></line>
|
||||
</Icon>
|
||||
)
|
||||
|
||||
export const BoxIcon = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
||||
</Icon>
|
||||
)
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,12 @@
|
||||
import { useToast } from '@chakra-ui/react'
|
||||
import { PublicTypebot, Settings, Theme, Typebot, Webhook } from 'models'
|
||||
import {
|
||||
LogicStepType,
|
||||
PublicTypebot,
|
||||
Settings,
|
||||
Theme,
|
||||
Typebot,
|
||||
Webhook,
|
||||
} from 'models'
|
||||
import { Router, useRouter } from 'next/router'
|
||||
import {
|
||||
createContext,
|
||||
@ -23,7 +30,7 @@ import {
|
||||
} from 'services/typebots/typebots'
|
||||
import { fetcher, preventUserFromRefreshing } from 'services/utils'
|
||||
import useSWR from 'swr'
|
||||
import { isDefined, isNotDefined } from 'utils'
|
||||
import { isDefined, isNotDefined, omit } from 'utils'
|
||||
import { BlocksActions, blocksActions } from './actions/blocks'
|
||||
import { stepsAction, StepsActions } from './actions/steps'
|
||||
import { variablesAction, VariablesActions } from './actions/variables'
|
||||
@ -36,6 +43,7 @@ import { generate } from 'short-uuid'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { User } from 'db'
|
||||
import { saveWebhook } from 'services/webhook'
|
||||
import { stringify } from 'qs'
|
||||
const autoSaveTimeout = 10000
|
||||
|
||||
type UpdateTypebotPayload = Partial<{
|
||||
@ -46,11 +54,14 @@ type UpdateTypebotPayload = Partial<{
|
||||
publishedTypebotId: string
|
||||
}>
|
||||
|
||||
export type SetTypebot = (typebot: Typebot | undefined) => void
|
||||
export type SetTypebot = (
|
||||
newPresent: Typebot | ((current: Typebot) => Typebot)
|
||||
) => void
|
||||
const typebotContext = createContext<
|
||||
{
|
||||
typebot?: Typebot
|
||||
publishedTypebot?: PublicTypebot
|
||||
linkedTypebots?: Typebot[]
|
||||
owner?: User
|
||||
webhooks: Webhook[]
|
||||
isReadOnly?: boolean
|
||||
@ -118,6 +129,7 @@ export const TypebotContext = ({
|
||||
{
|
||||
redo,
|
||||
undo,
|
||||
flush,
|
||||
canRedo,
|
||||
canUndo,
|
||||
set: setLocalTypebot,
|
||||
@ -125,10 +137,50 @@ export const TypebotContext = ({
|
||||
},
|
||||
] = useUndo<Typebot | undefined>(undefined)
|
||||
|
||||
const saveTypebot = async () => {
|
||||
const typebotToSave = currentTypebotRef.current
|
||||
if (deepEqual(typebot, typebotToSave)) return
|
||||
if (!typebotToSave) return
|
||||
const linkedTypebotIds = localTypebot?.blocks
|
||||
.flatMap((b) => b.steps)
|
||||
.reduce<string[]>(
|
||||
(typebotIds, step) =>
|
||||
step.type === LogicStepType.TYPEBOT_LINK &&
|
||||
isDefined(step.options.typebotId)
|
||||
? [...typebotIds, step.options.typebotId]
|
||||
: typebotIds,
|
||||
[]
|
||||
)
|
||||
|
||||
const { typebots: linkedTypebots } = useLinkedTypebots({
|
||||
typebotId,
|
||||
typebotIds: linkedTypebotIds,
|
||||
onError: (error) =>
|
||||
toast({
|
||||
title: 'Error while fetching linkedTypebots',
|
||||
description: error.message,
|
||||
}),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!typebot || !currentTypebotRef.current) return
|
||||
if (typebotId !== currentTypebotRef.current.id) {
|
||||
setLocalTypebot({ ...typebot })
|
||||
flush()
|
||||
} else if (
|
||||
new Date(typebot.updatedAt) >
|
||||
new Date(currentTypebotRef.current.updatedAt)
|
||||
) {
|
||||
setLocalTypebot({ ...typebot })
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [typebot])
|
||||
|
||||
const saveTypebot = async (options?: { disableMutation: boolean }) => {
|
||||
if (!currentTypebotRef.current || !typebot) return
|
||||
const typebotToSave = {
|
||||
...currentTypebotRef.current,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
if (deepEqual(omit(typebot, 'updatedAt'), omit(typebotToSave, 'updatedAt')))
|
||||
return
|
||||
setIsSavingLoading(true)
|
||||
const { error } = await updateTypebot(typebotToSave.id, typebotToSave)
|
||||
setIsSavingLoading(false)
|
||||
@ -136,7 +188,12 @@ export const TypebotContext = ({
|
||||
toast({ title: error.name, description: error.message })
|
||||
return
|
||||
}
|
||||
mutate({ typebot: typebotToSave, webhooks: webhooks ?? [] })
|
||||
if (!options?.disableMutation)
|
||||
mutate({
|
||||
typebot: typebotToSave,
|
||||
publishedTypebot,
|
||||
webhooks: webhooks ?? [],
|
||||
})
|
||||
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
}
|
||||
|
||||
@ -165,9 +222,10 @@ export const TypebotContext = ({
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
Router.events.on('routeChangeStart', saveTypebot)
|
||||
const save = () => saveTypebot({ disableMutation: true })
|
||||
Router.events.on('routeChangeStart', save)
|
||||
return () => {
|
||||
Router.events.off('routeChangeStart', saveTypebot)
|
||||
Router.events.off('routeChangeStart', save)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [typebot, publishedTypebot, webhooks])
|
||||
@ -177,10 +235,10 @@ export const TypebotContext = ({
|
||||
|
||||
const isPublished = useMemo(
|
||||
() =>
|
||||
isDefined(typebot) &&
|
||||
isDefined(localTypebot) &&
|
||||
isDefined(publishedTypebot) &&
|
||||
checkIfPublished(typebot, publishedTypebot),
|
||||
[typebot, publishedTypebot]
|
||||
checkIfPublished(localTypebot, publishedTypebot),
|
||||
[localTypebot, publishedTypebot]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@ -310,6 +368,7 @@ export const TypebotContext = ({
|
||||
value={{
|
||||
typebot: localTypebot,
|
||||
publishedTypebot,
|
||||
linkedTypebots,
|
||||
owner,
|
||||
webhooks: webhooks ?? [],
|
||||
isReadOnly,
|
||||
@ -326,11 +385,11 @@ export const TypebotContext = ({
|
||||
restorePublishedTypebot,
|
||||
updateOnBothTypebots,
|
||||
updateWebhook,
|
||||
...blocksActions(localTypebot as Typebot, setLocalTypebot),
|
||||
...stepsAction(localTypebot as Typebot, setLocalTypebot),
|
||||
...variablesAction(localTypebot as Typebot, setLocalTypebot),
|
||||
...edgesAction(localTypebot as Typebot, setLocalTypebot),
|
||||
...itemsAction(localTypebot as Typebot, setLocalTypebot),
|
||||
...blocksActions(setLocalTypebot as SetTypebot),
|
||||
...stepsAction(setLocalTypebot as SetTypebot),
|
||||
...variablesAction(setLocalTypebot as SetTypebot),
|
||||
...edgesAction(setLocalTypebot as SetTypebot),
|
||||
...itemsAction(setLocalTypebot as SetTypebot),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@ -356,7 +415,7 @@ export const useFetchedTypebot = ({
|
||||
isReadOnly?: boolean
|
||||
},
|
||||
Error
|
||||
>(`/api/typebots/${typebotId}`, fetcher)
|
||||
>(`/api/typebots/${typebotId}`, fetcher, { dedupingInterval: 0 })
|
||||
if (error) onError(error)
|
||||
return {
|
||||
typebot: data?.typebot,
|
||||
@ -369,6 +428,35 @@ export const useFetchedTypebot = ({
|
||||
}
|
||||
}
|
||||
|
||||
const useLinkedTypebots = ({
|
||||
typebotId,
|
||||
typebotIds,
|
||||
onError,
|
||||
}: {
|
||||
typebotId?: string
|
||||
typebotIds?: string[]
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const params = stringify({ typebotIds }, { indices: false })
|
||||
const { data, error, mutate } = useSWR<
|
||||
{
|
||||
typebots: Typebot[]
|
||||
},
|
||||
Error
|
||||
>(
|
||||
typebotIds?.every((id) => typebotId === id)
|
||||
? undefined
|
||||
: `/api/typebots?${params}`,
|
||||
fetcher
|
||||
)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
typebots: data?.typebots,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
const useAutoSave = <T,>(
|
||||
{
|
||||
handler,
|
||||
@ -376,7 +464,7 @@ const useAutoSave = <T,>(
|
||||
debounceTimeout,
|
||||
}: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
handler: (item?: T) => Promise<any>
|
||||
handler: () => Promise<any>
|
||||
item?: T
|
||||
debounceTimeout: number
|
||||
},
|
||||
@ -384,7 +472,7 @@ const useAutoSave = <T,>(
|
||||
) => {
|
||||
const [debouncedItem] = useDebounce(item, debounceTimeout)
|
||||
useEffect(() => {
|
||||
const save = () => handler(item)
|
||||
const save = () => handler()
|
||||
document.addEventListener('visibilitychange', save)
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', save)
|
||||
@ -392,7 +480,7 @@ const useAutoSave = <T,>(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, dependencies)
|
||||
return useEffect(() => {
|
||||
handler(item)
|
||||
handler()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedItem])
|
||||
}
|
||||
|
@ -24,10 +24,7 @@ export type BlocksActions = {
|
||||
deleteBlock: (blockIndex: number) => void
|
||||
}
|
||||
|
||||
const blocksActions = (
|
||||
typebot: Typebot,
|
||||
setTypebot: SetTypebot
|
||||
): BlocksActions => ({
|
||||
const blocksActions = (setTypebot: SetTypebot): BlocksActions => ({
|
||||
createBlock: ({
|
||||
id,
|
||||
step,
|
||||
@ -37,8 +34,8 @@ const blocksActions = (
|
||||
id: string
|
||||
step: DraggableStep | DraggableStepType
|
||||
indices: StepIndices
|
||||
}) => {
|
||||
setTypebot(
|
||||
}) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const newBlock: Block = {
|
||||
id,
|
||||
@ -49,17 +46,17 @@ const blocksActions = (
|
||||
typebot.blocks.push(newBlock)
|
||||
createStepDraft(typebot, step, newBlock.id, indices)
|
||||
})
|
||||
)
|
||||
},
|
||||
),
|
||||
updateBlock: (blockIndex: number, updates: Partial<Omit<Block, 'id'>>) =>
|
||||
setTypebot(
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const block = typebot.blocks[blockIndex]
|
||||
typebot.blocks[blockIndex] = { ...block, ...updates }
|
||||
})
|
||||
),
|
||||
|
||||
deleteBlock: (blockIndex: number) =>
|
||||
setTypebot(
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
deleteBlockDraft(typebot)(blockIndex)
|
||||
})
|
||||
|
@ -11,12 +11,9 @@ export type EdgesActions = {
|
||||
deleteEdge: (edgeId: string) => void
|
||||
}
|
||||
|
||||
export const edgesAction = (
|
||||
typebot: Typebot,
|
||||
setTypebot: SetTypebot
|
||||
): EdgesActions => ({
|
||||
createEdge: (edge: Omit<Edge, 'id'>) => {
|
||||
setTypebot(
|
||||
export const edgesAction = (setTypebot: SetTypebot): EdgesActions => ({
|
||||
createEdge: (edge: Omit<Edge, 'id'>) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const newEdge = {
|
||||
...edge,
|
||||
@ -45,10 +42,9 @@ export const edgesAction = (
|
||||
stepIndex,
|
||||
})
|
||||
})
|
||||
)
|
||||
},
|
||||
),
|
||||
updateEdge: (edgeIndex: number, updates: Partial<Omit<Edge, 'id'>>) =>
|
||||
setTypebot(
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const currentEdge = typebot.edges[edgeIndex]
|
||||
typebot.edges[edgeIndex] = {
|
||||
@ -57,13 +53,12 @@ export const edgesAction = (
|
||||
}
|
||||
})
|
||||
),
|
||||
deleteEdge: (edgeId: string) => {
|
||||
setTypebot(
|
||||
deleteEdge: (edgeId: string) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
deleteEdgeDraft(typebot, edgeId)
|
||||
})
|
||||
)
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
const addEdgeIdToStep = (
|
||||
|
@ -1,5 +1,4 @@
|
||||
import {
|
||||
Typebot,
|
||||
ItemIndices,
|
||||
Item,
|
||||
InputStepType,
|
||||
@ -18,15 +17,12 @@ export type ItemsActions = {
|
||||
deleteItem: (indices: ItemIndices) => void
|
||||
}
|
||||
|
||||
const itemsAction = (
|
||||
typebot: Typebot,
|
||||
setTypebot: SetTypebot
|
||||
): ItemsActions => ({
|
||||
const itemsAction = (setTypebot: SetTypebot): ItemsActions => ({
|
||||
createItem: (
|
||||
item: Omit<ButtonItem, 'id'>,
|
||||
{ blockIndex, stepIndex, itemIndex }: ItemIndices
|
||||
) => {
|
||||
setTypebot(
|
||||
) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const step = typebot.blocks[blockIndex].steps[stepIndex]
|
||||
if (step.type !== InputStepType.CHOICE) return
|
||||
@ -36,13 +32,12 @@ const itemsAction = (
|
||||
id: generate(),
|
||||
})
|
||||
})
|
||||
)
|
||||
},
|
||||
),
|
||||
updateItem: (
|
||||
{ blockIndex, stepIndex, itemIndex }: ItemIndices,
|
||||
updates: Partial<Omit<Item, 'id'>>
|
||||
) =>
|
||||
setTypebot(
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const step = typebot.blocks[blockIndex].steps[stepIndex]
|
||||
if (!stepHasItems(step)) return
|
||||
@ -54,8 +49,9 @@ const itemsAction = (
|
||||
} as Item
|
||||
})
|
||||
),
|
||||
deleteItem: ({ blockIndex, stepIndex, itemIndex }: ItemIndices) => {
|
||||
setTypebot(
|
||||
|
||||
deleteItem: ({ blockIndex, stepIndex, itemIndex }: ItemIndices) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const step = typebot.blocks[blockIndex].steps[
|
||||
stepIndex
|
||||
@ -64,8 +60,7 @@ const itemsAction = (
|
||||
step.items.splice(itemIndex, 1)
|
||||
cleanUpEdgeDraft(typebot, removingItem.id)
|
||||
})
|
||||
)
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
export { itemsAction }
|
||||
|
@ -26,42 +26,36 @@ export type StepsActions = {
|
||||
deleteStep: (indices: StepIndices) => void
|
||||
}
|
||||
|
||||
const stepsAction = (
|
||||
typebot: Typebot,
|
||||
setTypebot: SetTypebot
|
||||
): StepsActions => ({
|
||||
const stepsAction = (setTypebot: SetTypebot): StepsActions => ({
|
||||
createStep: (
|
||||
blockId: string,
|
||||
step: DraggableStep | DraggableStepType,
|
||||
indices: StepIndices
|
||||
) => {
|
||||
setTypebot(
|
||||
) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
createStepDraft(typebot, step, blockId, indices)
|
||||
})
|
||||
)
|
||||
},
|
||||
),
|
||||
updateStep: (
|
||||
{ blockIndex, stepIndex }: StepIndices,
|
||||
updates: Partial<Omit<Step, 'id' | 'type'>>
|
||||
) =>
|
||||
setTypebot(
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
const step = typebot.blocks[blockIndex].steps[stepIndex]
|
||||
typebot.blocks[blockIndex].steps[stepIndex] = { ...step, ...updates }
|
||||
})
|
||||
),
|
||||
detachStepFromBlock: (indices: StepIndices) => {
|
||||
setTypebot(produce(typebot, removeStepFromBlock(indices)))
|
||||
},
|
||||
deleteStep: ({ blockIndex, stepIndex }: StepIndices) => {
|
||||
setTypebot(
|
||||
detachStepFromBlock: (indices: StepIndices) =>
|
||||
setTypebot((typebot) => produce(typebot, removeStepFromBlock(indices))),
|
||||
deleteStep: ({ blockIndex, stepIndex }: StepIndices) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
removeStepFromBlock({ blockIndex, stepIndex })(typebot)
|
||||
removeEmptyBlocks(typebot)
|
||||
})
|
||||
)
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
const removeStepFromBlock =
|
||||
|
@ -12,35 +12,30 @@ export type VariablesActions = {
|
||||
deleteVariable: (variableId: string) => void
|
||||
}
|
||||
|
||||
export const variablesAction = (
|
||||
typebot: Typebot,
|
||||
setTypebot: SetTypebot
|
||||
): VariablesActions => ({
|
||||
createVariable: (newVariable: Variable) => {
|
||||
setTypebot(
|
||||
export const variablesAction = (setTypebot: SetTypebot): VariablesActions => ({
|
||||
createVariable: (newVariable: Variable) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
typebot.variables.push(newVariable)
|
||||
})
|
||||
)
|
||||
},
|
||||
),
|
||||
updateVariable: (
|
||||
variableId: string,
|
||||
updates: Partial<Omit<Variable, 'id'>>
|
||||
) =>
|
||||
setTypebot(
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
typebot.variables.map((v) =>
|
||||
v.id === variableId ? { ...v, ...updates } : v
|
||||
)
|
||||
})
|
||||
),
|
||||
deleteVariable: (itemId: string) => {
|
||||
setTypebot(
|
||||
deleteVariable: (itemId: string) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
deleteVariableDraft(typebot, itemId)
|
||||
})
|
||||
)
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
export const deleteVariableDraft = (
|
||||
|
@ -13,6 +13,7 @@ import { unparse } from 'papaparse'
|
||||
import { UnlockProPlanInfo } from 'components/shared/Info'
|
||||
import { LogsModal } from './LogsModal'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { isDefined } from 'utils'
|
||||
|
||||
type Props = {
|
||||
typebotId: string
|
||||
@ -26,7 +27,7 @@ export const SubmissionsContent = ({
|
||||
totalHiddenResults,
|
||||
onDeleteResults,
|
||||
}: Props) => {
|
||||
const { publishedTypebot } = useTypebot()
|
||||
const { publishedTypebot, linkedTypebots } = useTypebot()
|
||||
const [selectedIndices, setSelectedIndices] = useState<number[]>([])
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false)
|
||||
const [isExportLoading, setIsExportLoading] = useState(false)
|
||||
@ -37,6 +38,17 @@ export const SubmissionsContent = ({
|
||||
status: 'error',
|
||||
})
|
||||
|
||||
const blocksAndVariables = {
|
||||
blocks: [
|
||||
...(publishedTypebot?.blocks ?? []),
|
||||
...(linkedTypebots?.flatMap((t) => t.blocks) ?? []),
|
||||
].filter(isDefined),
|
||||
variables: [
|
||||
...(publishedTypebot?.variables ?? []),
|
||||
...(linkedTypebots?.flatMap((t) => t.variables) ?? []),
|
||||
].filter(isDefined),
|
||||
}
|
||||
|
||||
const { data, mutate, setSize, hasMore } = useResults({
|
||||
typebotId,
|
||||
onError: (err) => toast({ title: err.name, description: err.message }),
|
||||
@ -105,13 +117,13 @@ export const SubmissionsContent = ({
|
||||
if (!publishedTypebot) return []
|
||||
const { data, error } = await getAllResults(typebotId)
|
||||
if (error) toast({ description: error.message, title: error.name })
|
||||
return convertResultsToTableData(publishedTypebot)(data?.results)
|
||||
return convertResultsToTableData(blocksAndVariables)(data?.results)
|
||||
}
|
||||
|
||||
const tableData: { [key: string]: string }[] = useMemo(
|
||||
() =>
|
||||
publishedTypebot
|
||||
? convertResultsToTableData(publishedTypebot)(results)
|
||||
? convertResultsToTableData(blocksAndVariables)(results)
|
||||
: [],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[results]
|
||||
@ -147,6 +159,7 @@ export const SubmissionsContent = ({
|
||||
</Flex>
|
||||
|
||||
<SubmissionsTable
|
||||
blocksAndVariables={blocksAndVariables}
|
||||
data={tableData}
|
||||
onNewSelection={handleNewSelection}
|
||||
onScrollToBottom={handleScrolledToBottom}
|
||||
|
@ -3,7 +3,6 @@ import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { methodNotAllowed } from 'utils'
|
||||
|
||||
const handler = (req: NextApiRequest, res: NextApiResponse) => {
|
||||
console.log(req.method)
|
||||
if (req.method === 'POST') {
|
||||
return res.status(200).send(req.body)
|
||||
}
|
||||
|
@ -11,7 +11,35 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (!user) return notAuthenticated(res)
|
||||
try {
|
||||
if (req.method === 'GET') {
|
||||
const folderId = req.query.folderId ? req.query.folderId.toString() : null
|
||||
const folderId = req.query.allFolders
|
||||
? undefined
|
||||
: req.query.folderId
|
||||
? req.query.folderId.toString()
|
||||
: null
|
||||
const typebotIds = req.query.typebotIds as string[] | undefined
|
||||
if (typebotIds) {
|
||||
const typebots = await prisma.typebot.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
ownerId: user.id,
|
||||
id: { in: typebotIds },
|
||||
},
|
||||
{
|
||||
id: { in: typebotIds },
|
||||
collaborators: {
|
||||
some: {
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: { name: true, id: true, blocks: true },
|
||||
})
|
||||
return res.send({ typebots })
|
||||
}
|
||||
const typebots = await prisma.typebot.findMany({
|
||||
where: {
|
||||
ownerId: user.id,
|
||||
|
21
apps/builder/pages/api/typebots/[typebotId]/blocks.ts
Normal file
21
apps/builder/pages/api/typebots/[typebotId]/blocks.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import prisma from 'libs/prisma'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { getAuthenticatedUser } from 'services/api/utils'
|
||||
import { methodNotAllowed, notAuthenticated, notFound } from 'utils'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const user = await getAuthenticatedUser(req)
|
||||
if (!user) return notAuthenticated(res)
|
||||
if (req.method === 'GET') {
|
||||
const typebotId = req.query.typebotId as string
|
||||
const typebot = await prisma.typebot.findUnique({
|
||||
where: { id_ownerId: { id: typebotId, ownerId: user.id } },
|
||||
})
|
||||
if (!typebot) return notFound(res)
|
||||
return res.send({ blocks: typebot.blocks })
|
||||
}
|
||||
methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
@ -0,0 +1,102 @@
|
||||
{
|
||||
"id": "cl0j7q9el0032b51al3rx6jo7",
|
||||
"createdAt": "2022-03-09T07:01:25.917Z",
|
||||
"updatedAt": "2022-03-09T07:01:25.917Z",
|
||||
"name": "My typebot",
|
||||
"ownerId": "cl0cfi60r0000381a2bft9yis",
|
||||
"publishedTypebotId": null,
|
||||
"folderId": null,
|
||||
"blocks": [
|
||||
{
|
||||
"id": "jPVDjQ5go4ZxmGCmApbcQf",
|
||||
"steps": [
|
||||
{
|
||||
"id": "nrCKsAYzbCogJanfxUavUV",
|
||||
"type": "start",
|
||||
"label": "Start",
|
||||
"blockId": "jPVDjQ5go4ZxmGCmApbcQf",
|
||||
"outgoingEdgeId": "8MazLBx8HbfKeYLADQkA3z"
|
||||
}
|
||||
],
|
||||
"title": "Start",
|
||||
"graphCoordinates": { "x": 0, "y": 0 }
|
||||
},
|
||||
{
|
||||
"id": "nyD3H7h6tEZqDmGwGciGV4",
|
||||
"graphCoordinates": { "x": 428, "y": 168 },
|
||||
"title": "Block #1",
|
||||
"steps": [
|
||||
{
|
||||
"id": "s8Pz7fPg4niG1JcvBS3CwAs",
|
||||
"blockId": "nyD3H7h6tEZqDmGwGciGV4",
|
||||
"type": "Typebot link",
|
||||
"options": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "jMbvgRQfXUaXg37LRNqRaJ",
|
||||
"graphCoordinates": { "x": 423, "y": 386 },
|
||||
"title": "Hello",
|
||||
"steps": [
|
||||
{
|
||||
"id": "scE368YFYn9cWU1RkQDFLUW",
|
||||
"blockId": "jMbvgRQfXUaXg37LRNqRaJ",
|
||||
"type": "text",
|
||||
"content": {
|
||||
"html": "<div>Hello world</div>",
|
||||
"richText": [
|
||||
{ "type": "p", "children": [{ "text": "Hello world" }] }
|
||||
],
|
||||
"plainText": "Hello world"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sem1do43KTkuvf49eqWcMgc",
|
||||
"blockId": "jMbvgRQfXUaXg37LRNqRaJ",
|
||||
"type": "text input",
|
||||
"options": {
|
||||
"isLong": false,
|
||||
"labels": { "button": "Send", "placeholder": "Type your answer..." }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"edges": [
|
||||
{
|
||||
"from": {
|
||||
"blockId": "jPVDjQ5go4ZxmGCmApbcQf",
|
||||
"stepId": "nrCKsAYzbCogJanfxUavUV"
|
||||
},
|
||||
"to": { "blockId": "nyD3H7h6tEZqDmGwGciGV4" },
|
||||
"id": "8MazLBx8HbfKeYLADQkA3z"
|
||||
}
|
||||
],
|
||||
"theme": {
|
||||
"chat": {
|
||||
"inputs": {
|
||||
"color": "#303235",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"placeholderColor": "#9095A0"
|
||||
},
|
||||
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
|
||||
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
|
||||
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
|
||||
},
|
||||
"general": { "font": "Open Sans", "background": { "type": "None" } }
|
||||
},
|
||||
"settings": {
|
||||
"general": {
|
||||
"isBrandingEnabled": true,
|
||||
"isNewResultOnRefreshEnabled": false
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
|
||||
},
|
||||
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
|
||||
},
|
||||
"publicId": null,
|
||||
"customDomain": null
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
{
|
||||
"id": "cl0iecee90042961arm5kb0f0",
|
||||
"createdAt": "2022-03-08T17:18:50.337Z",
|
||||
"updatedAt": "2022-03-08T21:05:28.825Z",
|
||||
"name": "Another typebot",
|
||||
"ownerId": "cl0cfi60r0000381a2bft9yis",
|
||||
"publishedTypebotId": null,
|
||||
"folderId": null,
|
||||
"blocks": [
|
||||
{
|
||||
"id": "p4ByLVoKiDRyRoPHKmcTfw",
|
||||
"steps": [
|
||||
{
|
||||
"id": "rw6smEWEJzHKbiVKLUKFvZ",
|
||||
"type": "start",
|
||||
"label": "Start",
|
||||
"blockId": "p4ByLVoKiDRyRoPHKmcTfw",
|
||||
"outgoingEdgeId": "1z3pfiatTUHbraD2uSoA3E"
|
||||
}
|
||||
],
|
||||
"title": "Start",
|
||||
"graphCoordinates": { "x": 0, "y": 0 }
|
||||
},
|
||||
{
|
||||
"id": "bg4QEJseUsTP496H27j5k2",
|
||||
"steps": [
|
||||
{
|
||||
"id": "s8ZeBL9p5za77eBmdKECLYq",
|
||||
"type": "text input",
|
||||
"blockId": "bg4QEJseUsTP496H27j5k2",
|
||||
"options": {
|
||||
"isLong": false,
|
||||
"labels": { "button": "Send", "placeholder": "Type your answer..." }
|
||||
},
|
||||
"outgoingEdgeId": "aEBnubX4EMx4Cse6xPAR1m"
|
||||
}
|
||||
],
|
||||
"title": "Block #1",
|
||||
"graphCoordinates": { "x": 366, "y": 191 }
|
||||
},
|
||||
{
|
||||
"id": "uhqCZSNbsYVFxop7Gc8xvn",
|
||||
"steps": [
|
||||
{
|
||||
"id": "smyHyeS6yaFaHHU44BNmN4n",
|
||||
"type": "text",
|
||||
"blockId": "uhqCZSNbsYVFxop7Gc8xvn",
|
||||
"content": {
|
||||
"html": "<div>Second block</div>",
|
||||
"richText": [
|
||||
{ "type": "p", "children": [{ "text": "Second block" }] }
|
||||
],
|
||||
"plainText": "Second block"
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "Block #2",
|
||||
"graphCoordinates": { "x": 793, "y": 99 }
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"edges": [
|
||||
{
|
||||
"id": "1z3pfiatTUHbraD2uSoA3E",
|
||||
"to": { "blockId": "bg4QEJseUsTP496H27j5k2" },
|
||||
"from": {
|
||||
"stepId": "rw6smEWEJzHKbiVKLUKFvZ",
|
||||
"blockId": "p4ByLVoKiDRyRoPHKmcTfw"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "aEBnubX4EMx4Cse6xPAR1m",
|
||||
"to": { "blockId": "uhqCZSNbsYVFxop7Gc8xvn" },
|
||||
"from": {
|
||||
"stepId": "s8ZeBL9p5za77eBmdKECLYq",
|
||||
"blockId": "bg4QEJseUsTP496H27j5k2"
|
||||
}
|
||||
}
|
||||
],
|
||||
"theme": {
|
||||
"chat": {
|
||||
"inputs": {
|
||||
"color": "#303235",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"placeholderColor": "#9095A0"
|
||||
},
|
||||
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
|
||||
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
|
||||
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
|
||||
},
|
||||
"general": { "font": "Open Sans", "background": { "type": "None" } }
|
||||
},
|
||||
"settings": {
|
||||
"general": {
|
||||
"isBrandingEnabled": true,
|
||||
"isNewResultOnRefreshEnabled": false
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
|
||||
},
|
||||
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
|
||||
},
|
||||
"publicId": null,
|
||||
"customDomain": null
|
||||
}
|
@ -169,8 +169,8 @@ const parseTestTypebot = (partialTypebot: Partial<Typebot>): Typebot => ({
|
||||
theme: defaultTheme,
|
||||
settings: defaultSettings,
|
||||
publicId: null,
|
||||
updatedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
publishedTypebotId: null,
|
||||
customDomain: null,
|
||||
variables: [{ id: 'var1', name: 'var1' }],
|
||||
|
@ -80,7 +80,7 @@ test.describe.parallel('Image bubble step', () => {
|
||||
|
||||
await page.click('text=Click to edit...')
|
||||
await page.click('text=Giphy')
|
||||
await page.click('img >> nth=3')
|
||||
await page.click('img >> nth=3', { force: true })
|
||||
await expect(page.locator('img[alt="Step image"]')).toHaveAttribute(
|
||||
'src',
|
||||
new RegExp('giphy.com/media', 'gm')
|
||||
|
62
apps/builder/playwright/tests/logic/typebotLink.spec.ts
Normal file
62
apps/builder/playwright/tests/logic/typebotLink.spec.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { typebotViewer } from '../../services/selectorUtils'
|
||||
import { generate } from 'short-uuid'
|
||||
import { importTypebotInDatabase } from '../../services/database'
|
||||
import path from 'path'
|
||||
|
||||
test('should be configurable', async ({ page }) => {
|
||||
const typebotId = generate()
|
||||
const linkedTypebotId = generate()
|
||||
await importTypebotInDatabase(
|
||||
path.join(__dirname, '../../fixtures/typebots/logic/linkTypebots/1.json'),
|
||||
{ id: typebotId }
|
||||
)
|
||||
await importTypebotInDatabase(
|
||||
path.join(__dirname, '../../fixtures/typebots/logic/linkTypebots/2.json'),
|
||||
{ id: linkedTypebotId }
|
||||
)
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Configure...')
|
||||
await page.click('input[placeholder="Select a typebot"]')
|
||||
await page.click('text=Another typebot')
|
||||
await expect(page.locator('input[value="Another typebot"]')).toBeVisible()
|
||||
await page.click('[aria-label="Navigate to typebot"]')
|
||||
await expect(page).toHaveURL(
|
||||
`/typebots/${linkedTypebotId}/edit?parentId=${typebotId}`
|
||||
)
|
||||
await page.click('[aria-label="Navigate back"]')
|
||||
await expect(page).toHaveURL(`/typebots/${typebotId}/edit`)
|
||||
await page.click('text=Jump in Another typebot')
|
||||
await expect(page.locator('input[value="Another typebot"]')).toBeVisible()
|
||||
await page.click('input[placeholder="Select a block"]')
|
||||
await page.click('text=Block #2')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await expect(typebotViewer(page).locator('text=Second block')).toBeVisible()
|
||||
|
||||
await page.click('[aria-label="Close"]')
|
||||
await page.click('text=Jump to Block #2 in Another typebot')
|
||||
await page.click('input[value="Block #2"]', { clickCount: 3 })
|
||||
await page.press('input[value="Block #2"]', 'Backspace')
|
||||
await page.click('button >> text=Start')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await typebotViewer(page).locator('input').fill('Hello there!')
|
||||
await typebotViewer(page).locator('input').press('Enter')
|
||||
await expect(typebotViewer(page).locator('text=Hello there!')).toBeVisible()
|
||||
|
||||
await page.click('[aria-label="Close"]')
|
||||
await page.click('text=Jump to Start in Another typebot')
|
||||
await page.click('input[value="Another typebot"]', { clickCount: 3 })
|
||||
await page.press('input[value="Another typebot"]', 'Backspace')
|
||||
await page.click('button >> text=My typebot')
|
||||
await page.click('input[placeholder="Select a block"]', {
|
||||
clickCount: 3,
|
||||
})
|
||||
await page.press('input[placeholder="Select a block"]', 'Backspace')
|
||||
await page.click('button >> text=Hello')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await expect(typebotViewer(page).locator('text=Hello world')).toBeVisible()
|
||||
})
|
@ -43,7 +43,7 @@ test.describe.parallel('Settings page', () => {
|
||||
await page.click('button:has-text("Typing emulation")')
|
||||
await page.fill('[data-testid="speed"] input', '350')
|
||||
await page.fill('[data-testid="max-delay"] input', '1.5')
|
||||
await page.uncheck(':nth-match(input[type="checkbox"], 2)', {
|
||||
await page.uncheck('input[type="checkbox"] >> nth=-1', {
|
||||
force: true,
|
||||
})
|
||||
await expect(page.locator('[data-testid="speed"]')).toBeHidden()
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PublicTypebot, Typebot } from 'models'
|
||||
import { Block, PublicTypebot, Typebot, Variable } from 'models'
|
||||
import shortId from 'short-uuid'
|
||||
import { HStack, Text } from '@chakra-ui/react'
|
||||
import { CalendarIcon, CodeIcon } from 'assets/icons'
|
||||
@ -18,8 +18,8 @@ export const parseTypebotToPublicTypebot = (
|
||||
theme: typebot.theme,
|
||||
variables: typebot.variables,
|
||||
customDomain: typebot.customDomain,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
|
||||
export const parsePublicTypebotToTypebot = (
|
||||
@ -64,10 +64,11 @@ type HeaderCell = {
|
||||
accessor: string
|
||||
}
|
||||
|
||||
export const parseSubmissionsColumns = (
|
||||
typebot: PublicTypebot
|
||||
): HeaderCell[] => {
|
||||
const parsedBlocks = parseBlocksHeaders(typebot)
|
||||
export const parseSubmissionsColumns = (blocksAndVariables: {
|
||||
blocks: Block[]
|
||||
variables: Variable[]
|
||||
}): HeaderCell[] => {
|
||||
const parsedBlocks = parseBlocksHeaders(blocksAndVariables)
|
||||
return [
|
||||
{
|
||||
Header: (
|
||||
@ -79,13 +80,19 @@ export const parseSubmissionsColumns = (
|
||||
accessor: 'Submitted at',
|
||||
},
|
||||
...parsedBlocks,
|
||||
...parseVariablesHeaders(typebot, parsedBlocks),
|
||||
...parseVariablesHeaders(blocksAndVariables.variables, parsedBlocks),
|
||||
]
|
||||
}
|
||||
|
||||
const parseBlocksHeaders = (typebot: PublicTypebot) =>
|
||||
typebot.blocks
|
||||
.filter((block) => typebot && block.steps.some((step) => isInputStep(step)))
|
||||
const parseBlocksHeaders = ({
|
||||
blocks,
|
||||
variables,
|
||||
}: {
|
||||
blocks: Block[]
|
||||
variables: Variable[]
|
||||
}) =>
|
||||
blocks
|
||||
.filter((block) => block.steps.some((step) => isInputStep(step)))
|
||||
.reduce<HeaderCell[]>((headers, block) => {
|
||||
const inputStep = block.steps.find((step) => isInputStep(step))
|
||||
if (
|
||||
@ -94,13 +101,13 @@ const parseBlocksHeaders = (typebot: PublicTypebot) =>
|
||||
headers.find(
|
||||
(h) =>
|
||||
h.accessor ===
|
||||
typebot.variables.find(byId(inputStep.options.variableId))?.name
|
||||
variables.find(byId(inputStep.options.variableId))?.name
|
||||
)
|
||||
)
|
||||
return headers
|
||||
const matchedVariableName =
|
||||
inputStep.options.variableId &&
|
||||
typebot.variables.find(byId(inputStep.options.variableId))?.name
|
||||
variables.find(byId(inputStep.options.variableId))?.name
|
||||
return [
|
||||
...headers,
|
||||
{
|
||||
@ -123,13 +130,13 @@ const parseBlocksHeaders = (typebot: PublicTypebot) =>
|
||||
}, [])
|
||||
|
||||
const parseVariablesHeaders = (
|
||||
typebot: PublicTypebot,
|
||||
variables: Variable[],
|
||||
parsedBlocks: {
|
||||
Header: JSX.Element
|
||||
accessor: string
|
||||
}[]
|
||||
) =>
|
||||
typebot.variables.reduce<HeaderCell[]>((headers, v) => {
|
||||
variables.reduce<HeaderCell[]>((headers, v) => {
|
||||
if (parsedBlocks.find((b) => b.accessor === v.name)) return headers
|
||||
return [
|
||||
...headers,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PublicTypebot, ResultWithAnswers, VariableWithValue } from 'models'
|
||||
import { Block, ResultWithAnswers, Variable, VariableWithValue } from 'models'
|
||||
import useSWRInfinite from 'swr/infinite'
|
||||
import { stringify } from 'qs'
|
||||
import { Answer } from 'db'
|
||||
@ -96,7 +96,7 @@ export const parseDateToReadable = (dateStr: string): string => {
|
||||
}
|
||||
|
||||
export const convertResultsToTableData =
|
||||
({ variables, blocks }: PublicTypebot) =>
|
||||
({ variables, blocks }: { variables: Variable[]; blocks: Block[] }) =>
|
||||
(results: ResultWithAnswers[] | undefined) =>
|
||||
(results ?? []).map((result) => ({
|
||||
'Submitted at': parseDateToReadable(result.createdAt),
|
||||
|
@ -56,12 +56,14 @@ export type TypebotInDashboard = Pick<
|
||||
>
|
||||
export const useTypebots = ({
|
||||
folderId,
|
||||
allFolders,
|
||||
onError,
|
||||
}: {
|
||||
folderId?: string
|
||||
allFolders?: boolean
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const params = stringify({ folderId })
|
||||
const params = stringify({ folderId, allFolders })
|
||||
const { data, error, mutate } = useSWR<
|
||||
{ typebots: TypebotInDashboard[] },
|
||||
Error
|
||||
@ -229,6 +231,8 @@ const parseDefaultStepOptions = (type: StepWithOptionsType): StepOptions => {
|
||||
return defaultRedirectOptions
|
||||
case LogicStepType.CODE:
|
||||
return defaultCodeOptions
|
||||
case LogicStepType.TYPEBOT_LINK:
|
||||
return {}
|
||||
case IntegrationStepType.GOOGLE_SHEETS:
|
||||
return defaultGoogleSheetsOptions
|
||||
case IntegrationStepType.GOOGLE_ANALYTICS:
|
||||
|
@ -8,12 +8,14 @@ enum ActionType {
|
||||
Undo = 'UNDO',
|
||||
Redo = 'REDO',
|
||||
Set = 'SET',
|
||||
Flush = 'FLUSH',
|
||||
}
|
||||
|
||||
export interface Actions<T> {
|
||||
set: (newPresent: T) => void
|
||||
set: (newPresent: T | ((current: T) => T)) => void
|
||||
undo: () => void
|
||||
redo: () => void
|
||||
flush: () => void
|
||||
canUndo: boolean
|
||||
canRedo: boolean
|
||||
presentRef: React.MutableRefObject<T>
|
||||
@ -85,7 +87,7 @@ const reducer = <T>(state: State<T>, action: Action<T>) => {
|
||||
// console.log(
|
||||
// diff(
|
||||
// JSON.parse(JSON.stringify(newPresent)),
|
||||
// JSON.parse(JSON.stringify(present))
|
||||
// present ? JSON.parse(JSON.stringify(present)) : {}
|
||||
// )
|
||||
// )
|
||||
return {
|
||||
@ -94,6 +96,9 @@ const reducer = <T>(state: State<T>, action: Action<T>) => {
|
||||
future: [],
|
||||
}
|
||||
}
|
||||
|
||||
case ActionType.Flush:
|
||||
return { ...initialState, present }
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,22 +111,33 @@ const useUndo = <T>(initialPresent: T): [State<T>, Actions<T>] => {
|
||||
|
||||
const canUndo = state.past.length !== 0
|
||||
const canRedo = state.future.length !== 0
|
||||
|
||||
const undo = useCallback(() => {
|
||||
if (canUndo) {
|
||||
dispatch({ type: ActionType.Undo })
|
||||
}
|
||||
}, [canUndo])
|
||||
|
||||
const redo = useCallback(() => {
|
||||
if (canRedo) {
|
||||
dispatch({ type: ActionType.Redo })
|
||||
}
|
||||
}, [canRedo])
|
||||
const set = useCallback((newPresent: T) => {
|
||||
presentRef.current = newPresent
|
||||
dispatch({ type: ActionType.Set, newPresent })
|
||||
|
||||
const set = useCallback((newPresent: T | ((current: T) => T)) => {
|
||||
const updatedTypebot =
|
||||
'id' in newPresent
|
||||
? newPresent
|
||||
: (newPresent as (current: T) => T)(presentRef.current)
|
||||
presentRef.current = updatedTypebot
|
||||
dispatch({ type: ActionType.Set, newPresent: updatedTypebot })
|
||||
}, [])
|
||||
|
||||
return [state, { set, undo, redo, canUndo, canRedo, presentRef }]
|
||||
const flush = useCallback(() => {
|
||||
dispatch({ type: ActionType.Flush })
|
||||
}, [])
|
||||
|
||||
return [state, { set, undo, redo, flush, canUndo, canRedo, presentRef }]
|
||||
}
|
||||
|
||||
export default useUndo
|
||||
|
@ -44,7 +44,7 @@ const getTypebotFromPublicId = async (publicId?: string) => {
|
||||
where: { publicId },
|
||||
})
|
||||
if (isNotDefined(typebot)) return null
|
||||
return omit(typebot as PublicTypebot, 'createdAt', 'updatedAt')
|
||||
return omit(typebot as unknown as PublicTypebot, 'createdAt', 'updatedAt')
|
||||
}
|
||||
|
||||
const getTypebotFromCustomDomain = async (customDomain: string) => {
|
||||
@ -52,7 +52,7 @@ const getTypebotFromCustomDomain = async (customDomain: string) => {
|
||||
where: { customDomain },
|
||||
})
|
||||
if (isNotDefined(typebot)) return null
|
||||
return omit(typebot as PublicTypebot, 'createdAt', 'updatedAt')
|
||||
return omit(typebot as unknown as PublicTypebot, 'createdAt', 'updatedAt')
|
||||
}
|
||||
|
||||
const App = ({ typebot, ...props }: TypebotPageProps) =>
|
||||
|
18
apps/viewer/pages/api/publicTypebots/[typebotId].ts
Normal file
18
apps/viewer/pages/api/publicTypebots/[typebotId].ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { withSentry } from '@sentry/nextjs'
|
||||
import prisma from 'libs/prisma'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { methodNotAllowed, notFound } from 'utils'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'GET') {
|
||||
const typebotId = req.query.typebotId as string
|
||||
const typebot = await prisma.publicTypebot.findUnique({
|
||||
where: { typebotId },
|
||||
})
|
||||
if (!typebot) return notFound(res)
|
||||
return res.send({ typebot })
|
||||
}
|
||||
methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default withSentry(handler)
|
@ -18,6 +18,7 @@ const config: PlaywrightTestConfig = {
|
||||
use: {
|
||||
actionTimeout: 0,
|
||||
baseURL: process.env.NEXT_PUBLIC_VIEWER_HOST,
|
||||
storageState: path.join(__dirname, 'playwright/proUser.json'),
|
||||
trace: 'on-first-retry',
|
||||
video: 'retain-on-failure',
|
||||
locale: 'en-US',
|
||||
|
74
apps/viewer/playwright/fixtures/typebots/linkTypebots/1.json
Normal file
74
apps/viewer/playwright/fixtures/typebots/linkTypebots/1.json
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"id": "cl0ibhi7s0018n21aarlmg0cm",
|
||||
"createdAt": "2022-03-08T15:58:49.720Z",
|
||||
"updatedAt": "2022-03-08T16:07:18.899Z",
|
||||
"name": "My typebot",
|
||||
"ownerId": "cl0cfi60r0000381a2bft9yis",
|
||||
"publishedTypebotId": null,
|
||||
"folderId": null,
|
||||
"blocks": [
|
||||
{
|
||||
"id": "1qQrnsLzRim1LqCrhbj1MW",
|
||||
"steps": [
|
||||
{
|
||||
"id": "8srsGhdBJK8v88Xo1RRS4C",
|
||||
"type": "start",
|
||||
"label": "Start",
|
||||
"blockId": "1qQrnsLzRim1LqCrhbj1MW",
|
||||
"outgoingEdgeId": "ovUHhwr6THMhqtn8QbkjtA"
|
||||
}
|
||||
],
|
||||
"title": "Start",
|
||||
"graphCoordinates": { "x": 0, "y": 0 }
|
||||
},
|
||||
{
|
||||
"id": "wSR4VCcDNDTTsD9Szi2xH8",
|
||||
"steps": [
|
||||
{
|
||||
"id": "sw6nHJfkMsM4pxZxMBB6QqW",
|
||||
"type": "Typebot link",
|
||||
"blockId": "wSR4VCcDNDTTsD9Szi2xH8",
|
||||
"options": { "typebotId": "cl0ibhv8d0130n21aw8doxhj5" }
|
||||
}
|
||||
],
|
||||
"title": "Block #1",
|
||||
"graphCoordinates": { "x": 363, "y": 199 }
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"edges": [
|
||||
{
|
||||
"id": "ovUHhwr6THMhqtn8QbkjtA",
|
||||
"to": { "blockId": "wSR4VCcDNDTTsD9Szi2xH8" },
|
||||
"from": {
|
||||
"stepId": "8srsGhdBJK8v88Xo1RRS4C",
|
||||
"blockId": "1qQrnsLzRim1LqCrhbj1MW"
|
||||
}
|
||||
}
|
||||
],
|
||||
"theme": {
|
||||
"chat": {
|
||||
"inputs": {
|
||||
"color": "#303235",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"placeholderColor": "#9095A0"
|
||||
},
|
||||
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
|
||||
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
|
||||
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
|
||||
},
|
||||
"general": { "font": "Open Sans", "background": { "type": "None" } }
|
||||
},
|
||||
"settings": {
|
||||
"general": {
|
||||
"isBrandingEnabled": true,
|
||||
"isNewResultOnRefreshEnabled": false
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
|
||||
},
|
||||
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
|
||||
},
|
||||
"publicId": null,
|
||||
"customDomain": null
|
||||
}
|
77
apps/viewer/playwright/fixtures/typebots/linkTypebots/2.json
Normal file
77
apps/viewer/playwright/fixtures/typebots/linkTypebots/2.json
Normal file
@ -0,0 +1,77 @@
|
||||
{
|
||||
"id": "cl0ibhv8d0130n21aw8doxhj5",
|
||||
"createdAt": "2022-03-08T15:59:06.589Z",
|
||||
"updatedAt": "2022-03-08T15:59:10.498Z",
|
||||
"name": "Another typebot",
|
||||
"ownerId": "cl0cfi60r0000381a2bft9yis",
|
||||
"publishedTypebotId": null,
|
||||
"folderId": null,
|
||||
"blocks": [
|
||||
{
|
||||
"id": "p4ByLVoKiDRyRoPHKmcTfw",
|
||||
"steps": [
|
||||
{
|
||||
"id": "rw6smEWEJzHKbiVKLUKFvZ",
|
||||
"type": "start",
|
||||
"label": "Start",
|
||||
"blockId": "p4ByLVoKiDRyRoPHKmcTfw",
|
||||
"outgoingEdgeId": "1z3pfiatTUHbraD2uSoA3E"
|
||||
}
|
||||
],
|
||||
"title": "Start",
|
||||
"graphCoordinates": { "x": 0, "y": 0 }
|
||||
},
|
||||
{
|
||||
"id": "bg4QEJseUsTP496H27j5k2",
|
||||
"graphCoordinates": { "x": 366, "y": 191 },
|
||||
"title": "Block #1",
|
||||
"steps": [
|
||||
{
|
||||
"id": "s8ZeBL9p5za77eBmdKECLYq",
|
||||
"blockId": "bg4QEJseUsTP496H27j5k2",
|
||||
"type": "text input",
|
||||
"options": {
|
||||
"isLong": false,
|
||||
"labels": { "button": "Send", "placeholder": "Type your answer..." }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"edges": [
|
||||
{
|
||||
"from": {
|
||||
"blockId": "p4ByLVoKiDRyRoPHKmcTfw",
|
||||
"stepId": "rw6smEWEJzHKbiVKLUKFvZ"
|
||||
},
|
||||
"to": { "blockId": "bg4QEJseUsTP496H27j5k2" },
|
||||
"id": "1z3pfiatTUHbraD2uSoA3E"
|
||||
}
|
||||
],
|
||||
"theme": {
|
||||
"chat": {
|
||||
"inputs": {
|
||||
"color": "#303235",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"placeholderColor": "#9095A0"
|
||||
},
|
||||
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
|
||||
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
|
||||
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
|
||||
},
|
||||
"general": { "font": "Open Sans", "background": { "type": "None" } }
|
||||
},
|
||||
"settings": {
|
||||
"general": {
|
||||
"isBrandingEnabled": true,
|
||||
"isNewResultOnRefreshEnabled": false
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
|
||||
},
|
||||
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
|
||||
},
|
||||
"publicId": null,
|
||||
"customDomain": null
|
||||
}
|
18
apps/viewer/playwright/proUser.json
Normal file
18
apps/viewer/playwright/proUser.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"cookies": [],
|
||||
"origins": [
|
||||
{
|
||||
"origin": "http://localhost:3000",
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "authenticatedUser",
|
||||
"value": "{\"id\":\"proUser\",\"name\":\"Pro user\",\"email\":\"pro-user@email.com\",\"emailVerified\":null,\"image\":\"https://avatars.githubusercontent.com/u/16015833?v=4\",\"plan\":\"PRO\",\"stripeId\":null}"
|
||||
},
|
||||
{
|
||||
"name": "typebot-20-modal",
|
||||
"value": "hide"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -13,9 +13,11 @@ const prisma = new PrismaClient()
|
||||
export const teardownDatabase = async () => {
|
||||
try {
|
||||
await prisma.user.delete({
|
||||
where: { id: 'user' },
|
||||
where: { id: 'proUser' },
|
||||
})
|
||||
} catch {}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -24,7 +26,7 @@ export const setupDatabase = () => createUser()
|
||||
export const createUser = () =>
|
||||
prisma.user.create({
|
||||
data: {
|
||||
id: 'user',
|
||||
id: 'proUser',
|
||||
email: 'user@email.com',
|
||||
name: 'User',
|
||||
apiToken: 'userToken',
|
||||
@ -73,13 +75,13 @@ const parseTestTypebot = (partialTypebot: Partial<Typebot>): Typebot => ({
|
||||
id: partialTypebot.id ?? 'typebot',
|
||||
folderId: null,
|
||||
name: 'My typebot',
|
||||
ownerId: 'user',
|
||||
ownerId: 'proUser',
|
||||
theme: defaultTheme,
|
||||
settings: defaultSettings,
|
||||
publicId: partialTypebot.id + '-public',
|
||||
publishedTypebotId: null,
|
||||
updatedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
customDomain: null,
|
||||
variables: [{ id: 'var1', name: 'var1' }],
|
||||
...partialTypebot,
|
||||
@ -135,7 +137,7 @@ export const importTypebotInDatabase = async (
|
||||
const typebot: any = {
|
||||
...JSON.parse(readFileSync(path).toString()),
|
||||
...updates,
|
||||
ownerId: 'user',
|
||||
ownerId: 'proUser',
|
||||
}
|
||||
await prisma.typebot.create({
|
||||
data: typebot,
|
||||
|
29
apps/viewer/playwright/tests/typebotLink.spec.ts
Normal file
29
apps/viewer/playwright/tests/typebotLink.spec.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import path from 'path'
|
||||
import { importTypebotInDatabase } from '../services/database'
|
||||
import { typebotViewer } from '../services/selectorUtils'
|
||||
|
||||
test('should work as expected', async ({ page }) => {
|
||||
const typebotId = 'cl0ibhi7s0018n21aarlmg0cm'
|
||||
const linkedTypebotId = 'cl0ibhv8d0130n21aw8doxhj5'
|
||||
await importTypebotInDatabase(
|
||||
path.join(__dirname, '../fixtures/typebots/linkTypebots/1.json'),
|
||||
{ id: typebotId, publicId: `${typebotId}-public` }
|
||||
)
|
||||
await importTypebotInDatabase(
|
||||
path.join(__dirname, '../fixtures/typebots/linkTypebots/2.json'),
|
||||
{ id: linkedTypebotId, publicId: `${linkedTypebotId}-public` }
|
||||
)
|
||||
|
||||
await page.goto(`/${typebotId}-public`)
|
||||
await typebotViewer(page).locator('input').fill('Hello there!')
|
||||
await typebotViewer(page).locator('input').press('Enter')
|
||||
await page.waitForResponse(
|
||||
(resp) =>
|
||||
resp.request().url().includes(`/api/typebots/t/results`) &&
|
||||
resp.status() === 200 &&
|
||||
resp.request().method() === 'PUT'
|
||||
)
|
||||
await page.goto(`http://localhost:3000/typebots/${typebotId}/results`)
|
||||
await expect(page.locator('text=Hello there!')).toBeVisible()
|
||||
})
|
Reference in New Issue
Block a user