2
0

feat(engine): Link typebot step

This commit is contained in:
Baptiste Arnaud
2022-03-09 15:12:00 +01:00
parent 1bcc8aee10
commit 7e61ab19eb
61 changed files with 1272 additions and 245 deletions

View File

@ -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: {

View File

@ -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}>

View File

@ -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'}
/>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -0,0 +1 @@
export { TypebotLinkSettingsForm } from './TypebotLinkSettingsForm'

View File

@ -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,

View File

@ -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>
)}

View File

@ -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 (

View File

@ -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>
)
}