feat(editor): ✨ Restore published version button
Had to migrate webhooks into a standalone table
This commit is contained in:
@@ -17,7 +17,6 @@ import {
|
|||||||
StepOptions,
|
StepOptions,
|
||||||
TextBubbleStep,
|
TextBubbleStep,
|
||||||
Webhook,
|
Webhook,
|
||||||
WebhookStep,
|
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import {
|
import {
|
||||||
@@ -95,11 +94,6 @@ export const StepSettings = ({
|
|||||||
const handleOptionsChange = (options: StepOptions) => {
|
const handleOptionsChange = (options: StepOptions) => {
|
||||||
onStepChange({ options } as Partial<Step>)
|
onStepChange({ options } as Partial<Step>)
|
||||||
}
|
}
|
||||||
const handleWebhookChange = (updates: Partial<Webhook>) => {
|
|
||||||
onStepChange({
|
|
||||||
webhook: { ...(step as WebhookStep).webhook, ...updates },
|
|
||||||
} as Partial<Step>)
|
|
||||||
}
|
|
||||||
const handleItemChange = (updates: Partial<ConditionItem>) => {
|
const handleItemChange = (updates: Partial<ConditionItem>) => {
|
||||||
onStepChange({
|
onStepChange({
|
||||||
items: [{ ...(step as ConditionStep).items[0], ...updates }],
|
items: [{ ...(step as ConditionStep).items[0], ...updates }],
|
||||||
@@ -208,7 +202,6 @@ export const StepSettings = ({
|
|||||||
<WebhookSettings
|
<WebhookSettings
|
||||||
step={step}
|
step={step}
|
||||||
onOptionsChange={handleOptionsChange}
|
onOptionsChange={handleOptionsChange}
|
||||||
onWebhookChange={handleWebhookChange}
|
|
||||||
onTestRequestClick={onTestRequestClick}
|
onTestRequestClick={onTestRequestClick}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,12 +18,11 @@ import {
|
|||||||
Sheet,
|
Sheet,
|
||||||
useSheets,
|
useSheets,
|
||||||
} from 'services/integrations'
|
} from 'services/integrations'
|
||||||
import { isDefined } from 'utils'
|
import { isDefined, omit } from 'utils'
|
||||||
import { SheetsDropdown } from './SheetsDropdown'
|
import { SheetsDropdown } from './SheetsDropdown'
|
||||||
import { SpreadsheetsDropdown } from './SpreadsheetDropdown'
|
import { SpreadsheetsDropdown } from './SpreadsheetDropdown'
|
||||||
import { CellWithValueStack } from './CellWithValueStack'
|
import { CellWithValueStack } from './CellWithValueStack'
|
||||||
import { CellWithVariableIdStack } from './CellWithVariableIdStack'
|
import { CellWithVariableIdStack } from './CellWithVariableIdStack'
|
||||||
import { omit } from 'services/utils'
|
|
||||||
import { CredentialsDropdown } from 'components/shared/CredentialsDropdown'
|
import { CredentialsDropdown } from 'components/shared/CredentialsDropdown'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo, useState } from 'react'
|
import React, { useEffect, useMemo, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionButton,
|
AccordionButton,
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
AccordionPanel,
|
AccordionPanel,
|
||||||
Button,
|
Button,
|
||||||
Flex,
|
Flex,
|
||||||
|
Spinner,
|
||||||
Stack,
|
Stack,
|
||||||
useToast,
|
useToast,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
@@ -17,9 +18,10 @@ import {
|
|||||||
KeyValue,
|
KeyValue,
|
||||||
WebhookOptions,
|
WebhookOptions,
|
||||||
VariableForTest,
|
VariableForTest,
|
||||||
Webhook,
|
|
||||||
ResponseVariableMapping,
|
ResponseVariableMapping,
|
||||||
WebhookStep,
|
WebhookStep,
|
||||||
|
defaultWebhookAttributes,
|
||||||
|
Webhook,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import { DropdownList } from 'components/shared/DropdownList'
|
import { DropdownList } from 'components/shared/DropdownList'
|
||||||
import { TableList, TableListItemProps } from 'components/shared/TableList'
|
import { TableList, TableListItemProps } from 'components/shared/TableList'
|
||||||
@@ -32,21 +34,21 @@ import {
|
|||||||
import { HeadersInputs, QueryParamsInputs } from './KeyValueInputs'
|
import { HeadersInputs, QueryParamsInputs } from './KeyValueInputs'
|
||||||
import { VariableForTestInputs } from './VariableForTestInputs'
|
import { VariableForTestInputs } from './VariableForTestInputs'
|
||||||
import { DataVariableInputs } from './ResponseMappingInputs'
|
import { DataVariableInputs } from './ResponseMappingInputs'
|
||||||
|
import { byId } from 'utils'
|
||||||
|
import { deepEqual } from 'fast-equals'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
step: WebhookStep
|
step: WebhookStep
|
||||||
onOptionsChange: (options: WebhookOptions) => void
|
onOptionsChange: (options: WebhookOptions) => void
|
||||||
onWebhookChange: (updates: Partial<Webhook>) => void
|
|
||||||
onTestRequestClick: () => void
|
onTestRequestClick: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WebhookSettings = ({
|
export const WebhookSettings = ({
|
||||||
step: { webhook, options, blockId, id: stepId },
|
step: { options, blockId, id: stepId, webhookId },
|
||||||
onOptionsChange,
|
onOptionsChange,
|
||||||
onWebhookChange,
|
|
||||||
onTestRequestClick,
|
onTestRequestClick,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { typebot, save } = useTypebot()
|
const { typebot, save, webhooks, updateWebhook } = useTypebot()
|
||||||
const [isTestResponseLoading, setIsTestResponseLoading] = useState(false)
|
const [isTestResponseLoading, setIsTestResponseLoading] = useState(false)
|
||||||
const [testResponse, setTestResponse] = useState<string>()
|
const [testResponse, setTestResponse] = useState<string>()
|
||||||
const [responseKeys, setResponseKeys] = useState<string[]>([])
|
const [responseKeys, setResponseKeys] = useState<string[]>([])
|
||||||
@@ -55,18 +57,52 @@ export const WebhookSettings = ({
|
|||||||
position: 'top-right',
|
position: 'top-right',
|
||||||
status: 'error',
|
status: 'error',
|
||||||
})
|
})
|
||||||
|
const [localWebhook, setLocalWebhook] = useState(
|
||||||
|
webhooks.find(byId(webhookId))
|
||||||
|
)
|
||||||
|
|
||||||
const handleUrlChange = (url?: string) => onWebhookChange({ url })
|
useEffect(() => {
|
||||||
|
const incomingWebhook = webhooks.find(byId(webhookId))
|
||||||
|
if (deepEqual(incomingWebhook, localWebhook)) return
|
||||||
|
setLocalWebhook(incomingWebhook)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [webhooks])
|
||||||
|
|
||||||
const handleMethodChange = (method: HttpMethod) => onWebhookChange({ method })
|
useEffect(() => {
|
||||||
|
if (!typebot) return
|
||||||
|
if (!localWebhook) {
|
||||||
|
const newWebhook = {
|
||||||
|
id: webhookId,
|
||||||
|
...defaultWebhookAttributes,
|
||||||
|
typebotId: typebot.id,
|
||||||
|
} as Webhook
|
||||||
|
updateWebhook(webhookId, newWebhook)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
setLocalWebhook((localWebhook) => {
|
||||||
|
if (!localWebhook) return
|
||||||
|
updateWebhook(webhookId, localWebhook).then()
|
||||||
|
return localWebhook
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleUrlChange = (url?: string) =>
|
||||||
|
localWebhook && setLocalWebhook({ ...localWebhook, url: url ?? null })
|
||||||
|
|
||||||
|
const handleMethodChange = (method: HttpMethod) =>
|
||||||
|
localWebhook && setLocalWebhook({ ...localWebhook, method })
|
||||||
|
|
||||||
const handleQueryParamsChange = (queryParams: KeyValue[]) =>
|
const handleQueryParamsChange = (queryParams: KeyValue[]) =>
|
||||||
onWebhookChange({ queryParams })
|
localWebhook && setLocalWebhook({ ...localWebhook, queryParams })
|
||||||
|
|
||||||
const handleHeadersChange = (headers: KeyValue[]) =>
|
const handleHeadersChange = (headers: KeyValue[]) =>
|
||||||
onWebhookChange({ headers })
|
localWebhook && setLocalWebhook({ ...localWebhook, headers })
|
||||||
|
|
||||||
const handleBodyChange = (body: string) => onWebhookChange({ body })
|
const handleBodyChange = (body: string) =>
|
||||||
|
localWebhook && setLocalWebhook({ ...localWebhook, body })
|
||||||
|
|
||||||
const handleVariablesChange = (variablesForTest: VariableForTest[]) =>
|
const handleVariablesChange = (variablesForTest: VariableForTest[]) =>
|
||||||
onOptionsChange({ ...options, variablesForTest })
|
onOptionsChange({ ...options, variablesForTest })
|
||||||
@@ -76,10 +112,10 @@ export const WebhookSettings = ({
|
|||||||
) => onOptionsChange({ ...options, responseVariableMapping })
|
) => onOptionsChange({ ...options, responseVariableMapping })
|
||||||
|
|
||||||
const handleTestRequestClick = async () => {
|
const handleTestRequestClick = async () => {
|
||||||
if (!typebot) return
|
if (!typebot || !localWebhook) return
|
||||||
setIsTestResponseLoading(true)
|
setIsTestResponseLoading(true)
|
||||||
onTestRequestClick()
|
onTestRequestClick()
|
||||||
await save()
|
await Promise.all([updateWebhook(localWebhook.id, localWebhook), save()])
|
||||||
const { data, error } = await executeWebhook(
|
const { data, error } = await executeWebhook(
|
||||||
typebot.id,
|
typebot.id,
|
||||||
convertVariableForTestToVariables(
|
convertVariableForTestToVariables(
|
||||||
@@ -100,19 +136,20 @@ export const WebhookSettings = ({
|
|||||||
[responseKeys]
|
[responseKeys]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!localWebhook) return <Spinner />
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Flex>
|
<Flex>
|
||||||
<DropdownList<HttpMethod>
|
<DropdownList<HttpMethod>
|
||||||
currentItem={webhook.method}
|
currentItem={localWebhook.method as HttpMethod}
|
||||||
onItemSelect={handleMethodChange}
|
onItemSelect={handleMethodChange}
|
||||||
items={Object.values(HttpMethod)}
|
items={Object.values(HttpMethod)}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<InputWithVariableButton
|
<InputWithVariableButton
|
||||||
placeholder="Your Webhook URL..."
|
placeholder="Your Webhook URL..."
|
||||||
initialValue={webhook.url ?? ''}
|
initialValue={localWebhook.url ?? ''}
|
||||||
onChange={handleUrlChange}
|
onChange={handleUrlChange}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -124,7 +161,7 @@ export const WebhookSettings = ({
|
|||||||
</AccordionButton>
|
</AccordionButton>
|
||||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||||
<TableList<KeyValue>
|
<TableList<KeyValue>
|
||||||
initialItems={webhook.queryParams}
|
initialItems={localWebhook.queryParams}
|
||||||
onItemsChange={handleQueryParamsChange}
|
onItemsChange={handleQueryParamsChange}
|
||||||
Item={QueryParamsInputs}
|
Item={QueryParamsInputs}
|
||||||
addLabel="Add a param"
|
addLabel="Add a param"
|
||||||
@@ -138,7 +175,7 @@ export const WebhookSettings = ({
|
|||||||
</AccordionButton>
|
</AccordionButton>
|
||||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||||
<TableList<KeyValue>
|
<TableList<KeyValue>
|
||||||
initialItems={webhook.headers}
|
initialItems={localWebhook.headers}
|
||||||
onItemsChange={handleHeadersChange}
|
onItemsChange={handleHeadersChange}
|
||||||
Item={HeadersInputs}
|
Item={HeadersInputs}
|
||||||
addLabel="Add a value"
|
addLabel="Add a value"
|
||||||
@@ -152,7 +189,7 @@ export const WebhookSettings = ({
|
|||||||
</AccordionButton>
|
</AccordionButton>
|
||||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={webhook.body ?? ''}
|
value={localWebhook.body ?? ''}
|
||||||
lang="json"
|
lang="json"
|
||||||
onChange={handleBodyChange}
|
onChange={handleBodyChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,23 +8,27 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { ExternalLinkIcon } from 'assets/icons'
|
import { ExternalLinkIcon } from 'assets/icons'
|
||||||
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
import { ZapierStep } from 'models'
|
import { ZapierStep } from 'models'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { byId } from 'utils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
step: ZapierStep
|
step: ZapierStep
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ZapierSettings = ({ step }: Props) => {
|
export const ZapierSettings = ({ step }: Props) => {
|
||||||
|
const { webhooks } = useTypebot()
|
||||||
|
const webhook = webhooks.find(byId(step.webhookId))
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<Alert
|
<Alert
|
||||||
status={step.webhook.url ? 'success' : 'info'}
|
status={webhook?.url ? 'success' : 'info'}
|
||||||
bgColor={step.webhook.url ? undefined : 'blue.50'}
|
bgColor={webhook?.url ? undefined : 'blue.50'}
|
||||||
rounded="md"
|
rounded="md"
|
||||||
>
|
>
|
||||||
<AlertIcon />
|
<AlertIcon />
|
||||||
{step.webhook.url ? (
|
{webhook?.url ? (
|
||||||
<>Your zap is correctly configured 🚀</>
|
<>Your zap is correctly configured 🚀</>
|
||||||
) : (
|
) : (
|
||||||
<Stack>
|
<Stack>
|
||||||
@@ -40,7 +44,7 @@ export const ZapierSettings = ({ step }: Props) => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Alert>
|
</Alert>
|
||||||
{step.webhook.url && <Input value={step.webhook.url} isDisabled />}
|
{webhook?.url && <Input value={webhook?.url} isDisabled />}
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { Text } from '@chakra-ui/react'
|
import { Text } from '@chakra-ui/react'
|
||||||
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
import { WebhookStep } from 'models'
|
import { WebhookStep } from 'models'
|
||||||
|
import { byId } from 'utils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
step: WebhookStep
|
step: WebhookStep
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WebhookContent = ({ step: { webhook } }: Props) => {
|
export const WebhookContent = ({ step: { webhookId } }: Props) => {
|
||||||
|
const { webhooks } = useTypebot()
|
||||||
|
const webhook = webhooks.find(byId(webhookId))
|
||||||
|
|
||||||
if (!webhook?.url) return <Text color="gray.500">Configure...</Text>
|
if (!webhook?.url) return <Text color="gray.500">Configure...</Text>
|
||||||
return (
|
return (
|
||||||
<Text isTruncated pr="6">
|
<Text isTruncated pr="6">
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import { Text } from '@chakra-ui/react'
|
import { Text } from '@chakra-ui/react'
|
||||||
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
import { ZapierStep } from 'models'
|
import { ZapierStep } from 'models'
|
||||||
import { isNotDefined } from 'utils'
|
import { byId, isNotDefined } from 'utils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
step: ZapierStep
|
step: ZapierStep
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ZapierContent = ({ step }: Props) => {
|
export const ZapierContent = ({ step: { webhookId } }: Props) => {
|
||||||
if (isNotDefined(step.webhook.body))
|
const { webhooks } = useTypebot()
|
||||||
|
const webhook = webhooks.find(byId(webhookId))
|
||||||
|
|
||||||
|
if (isNotDefined(webhook?.body))
|
||||||
return <Text color="gray.500">Configure...</Text>
|
return <Text color="gray.500">Configure...</Text>
|
||||||
return (
|
return (
|
||||||
<Text isTruncated pr="6">
|
<Text isTruncated pr="6">
|
||||||
{step.webhook.url ? 'Enabled' : 'Disabled'}
|
{webhook?.url ? 'Enabled' : 'Disabled'}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,75 @@
|
|||||||
import { Button } from '@chakra-ui/react'
|
import {
|
||||||
|
Button,
|
||||||
|
HStack,
|
||||||
|
IconButton,
|
||||||
|
Stack,
|
||||||
|
Tooltip,
|
||||||
|
Text,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuList,
|
||||||
|
MenuItem,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { ChevronLeftIcon } from 'assets/icons'
|
||||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||||
|
import { timeSince } from 'services/utils'
|
||||||
|
import { isNotDefined } from 'utils'
|
||||||
|
|
||||||
export const PublishButton = () => {
|
export const PublishButton = () => {
|
||||||
const { isPublishing, isPublished, publishTypebot } = useTypebot()
|
const {
|
||||||
|
isPublishing,
|
||||||
|
isPublished,
|
||||||
|
publishTypebot,
|
||||||
|
publishedTypebot,
|
||||||
|
restorePublishedTypebot,
|
||||||
|
} = useTypebot()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<HStack spacing="1px">
|
||||||
ml={2}
|
<Tooltip
|
||||||
colorScheme="blue"
|
borderRadius="md"
|
||||||
isLoading={isPublishing}
|
hasArrow
|
||||||
isDisabled={isPublished}
|
placement="bottom-end"
|
||||||
onClick={publishTypebot}
|
label={
|
||||||
>
|
<Stack>
|
||||||
{isPublished ? 'Published' : 'Publish'}
|
<Text>There are non published changes.</Text>
|
||||||
</Button>
|
<Text fontStyle="italic">
|
||||||
|
Published version from{' '}
|
||||||
|
{publishedTypebot &&
|
||||||
|
timeSince(publishedTypebot.updatedAt.toString())}{' '}
|
||||||
|
ago
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
isDisabled={isNotDefined(publishedTypebot)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
isLoading={isPublishing}
|
||||||
|
isDisabled={isPublished}
|
||||||
|
onClick={publishTypebot}
|
||||||
|
borderRightRadius={publishedTypebot && !isPublished ? 0 : undefined}
|
||||||
|
>
|
||||||
|
{isPublished ? 'Published' : 'Publish'}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{publishedTypebot && !isPublished && (
|
||||||
|
<Menu>
|
||||||
|
<MenuButton
|
||||||
|
as={IconButton}
|
||||||
|
colorScheme="blue"
|
||||||
|
borderLeftRadius={0}
|
||||||
|
icon={<ChevronLeftIcon transform="rotate(-90deg)" />}
|
||||||
|
aria-label="Show published version"
|
||||||
|
/>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem onClick={restorePublishedTypebot}>
|
||||||
|
Restore published version
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Block, Edge, IdMap, PublicBlock, Source, Step, Target } from 'models'
|
import { Block, Edge, IdMap, Source, Step, Target } from 'models'
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
Dispatch,
|
Dispatch,
|
||||||
@@ -88,7 +88,7 @@ export const GraphProvider = ({
|
|||||||
isReadOnly = false,
|
isReadOnly = false,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
blocks: (Block | PublicBlock)[]
|
blocks: Block[]
|
||||||
isReadOnly?: boolean
|
isReadOnly?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
|
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useToast } from '@chakra-ui/react'
|
import { useToast } from '@chakra-ui/react'
|
||||||
import { PublicTypebot, Settings, Theme, Typebot } from 'models'
|
import { PublicTypebot, Settings, Theme, Typebot, Webhook } from 'models'
|
||||||
import { Router, useRouter } from 'next/router'
|
import { Router, useRouter } from 'next/router'
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import {
|
import {
|
||||||
createPublishedTypebot,
|
createPublishedTypebot,
|
||||||
|
parsePublicTypebotToTypebot,
|
||||||
parseTypebotToPublicTypebot,
|
parseTypebotToPublicTypebot,
|
||||||
updatePublishedTypebot,
|
updatePublishedTypebot,
|
||||||
} from 'services/publicTypebot'
|
} from 'services/publicTypebot'
|
||||||
@@ -34,6 +35,7 @@ import { itemsAction, ItemsActions } from './actions/items'
|
|||||||
import { generate } from 'short-uuid'
|
import { generate } from 'short-uuid'
|
||||||
import { deepEqual } from 'fast-equals'
|
import { deepEqual } from 'fast-equals'
|
||||||
import { User } from 'db'
|
import { User } from 'db'
|
||||||
|
import { saveWebhook } from 'services/webhook'
|
||||||
const autoSaveTimeout = 10000
|
const autoSaveTimeout = 10000
|
||||||
|
|
||||||
type UpdateTypebotPayload = Partial<{
|
type UpdateTypebotPayload = Partial<{
|
||||||
@@ -50,6 +52,7 @@ const typebotContext = createContext<
|
|||||||
typebot?: Typebot
|
typebot?: Typebot
|
||||||
publishedTypebot?: PublicTypebot
|
publishedTypebot?: PublicTypebot
|
||||||
owner?: User
|
owner?: User
|
||||||
|
webhooks: Webhook[]
|
||||||
isReadOnly?: boolean
|
isReadOnly?: boolean
|
||||||
isPublished: boolean
|
isPublished: boolean
|
||||||
isPublishing: boolean
|
isPublishing: boolean
|
||||||
@@ -59,6 +62,10 @@ const typebotContext = createContext<
|
|||||||
redo: () => void
|
redo: () => void
|
||||||
canRedo: boolean
|
canRedo: boolean
|
||||||
canUndo: boolean
|
canUndo: boolean
|
||||||
|
updateWebhook: (
|
||||||
|
webhookId: string,
|
||||||
|
webhook: Partial<Webhook>
|
||||||
|
) => Promise<void>
|
||||||
updateTypebot: (updates: UpdateTypebotPayload) => void
|
updateTypebot: (updates: UpdateTypebotPayload) => void
|
||||||
updateOnBothTypebots: (updates: {
|
updateOnBothTypebots: (updates: {
|
||||||
publicId?: string
|
publicId?: string
|
||||||
@@ -66,6 +73,7 @@ const typebotContext = createContext<
|
|||||||
customDomain?: string | null
|
customDomain?: string | null
|
||||||
}) => void
|
}) => void
|
||||||
publishTypebot: () => void
|
publishTypebot: () => void
|
||||||
|
restorePublishedTypebot: () => void
|
||||||
} & BlocksActions &
|
} & BlocksActions &
|
||||||
StepsActions &
|
StepsActions &
|
||||||
ItemsActions &
|
ItemsActions &
|
||||||
@@ -88,27 +96,22 @@ export const TypebotContext = ({
|
|||||||
status: 'error',
|
status: 'error',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { typebot, publishedTypebot, owner, isReadOnly, isLoading, mutate } =
|
const {
|
||||||
useFetchedTypebot({
|
typebot,
|
||||||
typebotId,
|
publishedTypebot,
|
||||||
onError: (error) =>
|
owner,
|
||||||
toast({
|
webhooks,
|
||||||
title: 'Error while fetching typebot',
|
isReadOnly,
|
||||||
description: error.message,
|
isLoading,
|
||||||
}),
|
mutate,
|
||||||
})
|
} = useFetchedTypebot({
|
||||||
|
typebotId,
|
||||||
useEffect(() => {
|
onError: (error) =>
|
||||||
if (
|
toast({
|
||||||
!typebot ||
|
title: 'Error while fetching typebot',
|
||||||
!localTypebot ||
|
description: error.message,
|
||||||
typebot.updatedAt <= localTypebot.updatedAt ||
|
}),
|
||||||
deepEqual(typebot, localTypebot)
|
})
|
||||||
)
|
|
||||||
return
|
|
||||||
setLocalTypebot({ ...typebot })
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [typebot])
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
{ present: localTypebot },
|
{ present: localTypebot },
|
||||||
@@ -133,7 +136,7 @@ export const TypebotContext = ({
|
|||||||
toast({ title: error.name, description: error.message })
|
toast({ title: error.name, description: error.message })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mutate({ typebot: typebotToSave })
|
mutate({ typebot: typebotToSave, webhooks: webhooks ?? [] })
|
||||||
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +151,7 @@ export const TypebotContext = ({
|
|||||||
mutate({
|
mutate({
|
||||||
typebot: currentTypebotRef.current as Typebot,
|
typebot: currentTypebotRef.current as Typebot,
|
||||||
publishedTypebot: newPublishedTypebot,
|
publishedTypebot: newPublishedTypebot,
|
||||||
|
webhooks: webhooks ?? [],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +258,11 @@ export const TypebotContext = ({
|
|||||||
})
|
})
|
||||||
setIsPublishing(false)
|
setIsPublishing(false)
|
||||||
if (error) return toast({ title: error.name, description: error.message })
|
if (error) return toast({ title: error.name, description: error.message })
|
||||||
mutate({ typebot: localTypebot, publishedTypebot: data })
|
mutate({
|
||||||
|
typebot: localTypebot,
|
||||||
|
publishedTypebot: data,
|
||||||
|
webhooks: webhooks ?? [],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,12 +280,35 @@ export const TypebotContext = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const restorePublishedTypebot = () => {
|
||||||
|
if (!publishedTypebot || !localTypebot) return
|
||||||
|
setLocalTypebot(parsePublicTypebotToTypebot(publishedTypebot, localTypebot))
|
||||||
|
return saveTypebot()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateWebhook = async (
|
||||||
|
webhookId: string,
|
||||||
|
updates: Partial<Webhook>
|
||||||
|
) => {
|
||||||
|
if (!typebot) return
|
||||||
|
const { data } = await saveWebhook(webhookId, updates)
|
||||||
|
if (data)
|
||||||
|
mutate({
|
||||||
|
typebot,
|
||||||
|
publishedTypebot,
|
||||||
|
webhooks: (webhooks ?? []).map((w) =>
|
||||||
|
w.id === webhookId ? data.webhook : w
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<typebotContext.Provider
|
<typebotContext.Provider
|
||||||
value={{
|
value={{
|
||||||
typebot: localTypebot,
|
typebot: localTypebot,
|
||||||
publishedTypebot,
|
publishedTypebot,
|
||||||
owner,
|
owner,
|
||||||
|
webhooks: webhooks ?? [],
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
isSavingLoading,
|
isSavingLoading,
|
||||||
save: saveTypebot,
|
save: saveTypebot,
|
||||||
@@ -289,7 +320,9 @@ export const TypebotContext = ({
|
|||||||
isPublishing,
|
isPublishing,
|
||||||
isPublished,
|
isPublished,
|
||||||
updateTypebot: updateLocalTypebot,
|
updateTypebot: updateLocalTypebot,
|
||||||
|
restorePublishedTypebot,
|
||||||
updateOnBothTypebots,
|
updateOnBothTypebots,
|
||||||
|
updateWebhook,
|
||||||
...blocksActions(localTypebot as Typebot, setLocalTypebot),
|
...blocksActions(localTypebot as Typebot, setLocalTypebot),
|
||||||
...stepsAction(localTypebot as Typebot, setLocalTypebot),
|
...stepsAction(localTypebot as Typebot, setLocalTypebot),
|
||||||
...variablesAction(localTypebot as Typebot, setLocalTypebot),
|
...variablesAction(localTypebot as Typebot, setLocalTypebot),
|
||||||
@@ -314,6 +347,7 @@ export const useFetchedTypebot = ({
|
|||||||
const { data, error, mutate } = useSWR<
|
const { data, error, mutate } = useSWR<
|
||||||
{
|
{
|
||||||
typebot: Typebot
|
typebot: Typebot
|
||||||
|
webhooks: Webhook[]
|
||||||
publishedTypebot?: PublicTypebot
|
publishedTypebot?: PublicTypebot
|
||||||
owner?: User
|
owner?: User
|
||||||
isReadOnly?: boolean
|
isReadOnly?: boolean
|
||||||
@@ -323,6 +357,7 @@ export const useFetchedTypebot = ({
|
|||||||
if (error) onError(error)
|
if (error) onError(error)
|
||||||
return {
|
return {
|
||||||
typebot: data?.typebot,
|
typebot: data?.typebot,
|
||||||
|
webhooks: data?.webhooks,
|
||||||
publishedTypebot: data?.publishedTypebot,
|
publishedTypebot: data?.publishedTypebot,
|
||||||
owner: data?.owner,
|
owner: data?.owner,
|
||||||
isReadOnly: data?.isReadOnly,
|
isReadOnly: data?.isReadOnly,
|
||||||
|
|||||||
@@ -54,10 +54,10 @@ const stepsAction = (
|
|||||||
detachStepFromBlock: (indices: StepIndices) => {
|
detachStepFromBlock: (indices: StepIndices) => {
|
||||||
setTypebot(produce(typebot, removeStepFromBlock(indices)))
|
setTypebot(produce(typebot, removeStepFromBlock(indices)))
|
||||||
},
|
},
|
||||||
deleteStep: (indices: StepIndices) => {
|
deleteStep: ({ blockIndex, stepIndex }: StepIndices) => {
|
||||||
setTypebot(
|
setTypebot(
|
||||||
produce(typebot, (typebot) => {
|
produce(typebot, (typebot) => {
|
||||||
removeStepFromBlock(indices)(typebot)
|
removeStepFromBlock({ blockIndex, stepIndex })(typebot)
|
||||||
removeEmptyBlocks(typebot)
|
removeEmptyBlocks(typebot)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -91,7 +91,7 @@ const createStepDraft = (
|
|||||||
removeEmptyBlocks(typebot)
|
removeEmptyBlocks(typebot)
|
||||||
}
|
}
|
||||||
|
|
||||||
const createNewStep = (
|
const createNewStep = async (
|
||||||
typebot: WritableDraft<Typebot>,
|
typebot: WritableDraft<Typebot>,
|
||||||
type: DraggableStepType,
|
type: DraggableStepType,
|
||||||
blockId: string,
|
blockId: string,
|
||||||
|
|||||||
@@ -22,10 +22,17 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
publishedTypebot: true,
|
publishedTypebot: true,
|
||||||
owner: { select: { email: true, name: true, image: true } },
|
owner: { select: { email: true, name: true, image: true } },
|
||||||
collaborators: { select: { userId: true, type: true } },
|
collaborators: { select: { userId: true, type: true } },
|
||||||
|
webhooks: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!typebot) return res.send({ typebot: null })
|
if (!typebot) return res.send({ typebot: null })
|
||||||
const { publishedTypebot, owner, collaborators, ...restOfTypebot } = typebot
|
const {
|
||||||
|
publishedTypebot,
|
||||||
|
owner,
|
||||||
|
collaborators,
|
||||||
|
webhooks,
|
||||||
|
...restOfTypebot
|
||||||
|
} = typebot
|
||||||
const isReadOnly =
|
const isReadOnly =
|
||||||
collaborators.find((c) => c.userId === user.id)?.type ===
|
collaborators.find((c) => c.userId === user.id)?.type ===
|
||||||
CollaborationType.READ
|
CollaborationType.READ
|
||||||
@@ -34,6 +41,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
publishedTypebot,
|
publishedTypebot,
|
||||||
owner,
|
owner,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
|
webhooks,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
apps/builder/pages/api/typebots/[typebotId]/webhooks.ts
Normal file
21
apps/builder/pages/api/typebots/[typebotId]/webhooks.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { withSentry } from '@sentry/nextjs'
|
||||||
|
import prisma from 'libs/prisma'
|
||||||
|
import { defaultWebhookAttributes } from 'models'
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import { getAuthenticatedUser } from 'services/api/utils'
|
||||||
|
import { methodNotAllowed, notAuthenticated } from 'utils'
|
||||||
|
|
||||||
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
const user = await getAuthenticatedUser(req)
|
||||||
|
if (!user) return notAuthenticated(res)
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
const typebotId = req.query.typebotId as string
|
||||||
|
const webhook = await prisma.webhook.create({
|
||||||
|
data: { typebotId, ...defaultWebhookAttributes },
|
||||||
|
})
|
||||||
|
return res.send({ webhook })
|
||||||
|
}
|
||||||
|
methodNotAllowed(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withSentry(handler)
|
||||||
68
apps/builder/pages/api/webhooks/[webhookId].ts
Normal file
68
apps/builder/pages/api/webhooks/[webhookId].ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { withSentry } from '@sentry/nextjs'
|
||||||
|
import { CollaborationType } from 'db'
|
||||||
|
import prisma from 'libs/prisma'
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import { getAuthenticatedUser } from 'services/api/utils'
|
||||||
|
import {
|
||||||
|
badRequest,
|
||||||
|
forbidden,
|
||||||
|
methodNotAllowed,
|
||||||
|
notAuthenticated,
|
||||||
|
} from 'utils'
|
||||||
|
|
||||||
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
const user = await getAuthenticatedUser(req)
|
||||||
|
if (!user) return notAuthenticated(res)
|
||||||
|
const webhookId = req.query.webhookId as string
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
const webhook = await prisma.webhook.findFirst({
|
||||||
|
where: {
|
||||||
|
id: webhookId,
|
||||||
|
typebot: {
|
||||||
|
OR: [
|
||||||
|
{ ownerId: user.id },
|
||||||
|
{
|
||||||
|
collaborators: {
|
||||||
|
some: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return res.send({ webhook })
|
||||||
|
}
|
||||||
|
if (req.method === 'PUT') {
|
||||||
|
const data = req.body
|
||||||
|
if (!('typebotId' in data)) return badRequest(res)
|
||||||
|
const typebot = await prisma.typebot.findFirst({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ id: data.typebotId, ownerId: user.id },
|
||||||
|
{
|
||||||
|
collaborators: {
|
||||||
|
some: {
|
||||||
|
userId: user.id,
|
||||||
|
type: CollaborationType.WRITE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (!typebot) return forbidden(res)
|
||||||
|
const webhook = await prisma.webhook.upsert({
|
||||||
|
where: {
|
||||||
|
id: webhookId,
|
||||||
|
},
|
||||||
|
create: data,
|
||||||
|
update: data,
|
||||||
|
})
|
||||||
|
return res.send({ webhook })
|
||||||
|
}
|
||||||
|
methodNotAllowed(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withSentry(handler)
|
||||||
@@ -63,12 +63,7 @@
|
|||||||
"blockId": "8XnDM1QsqPms4LQHh8q3Jo",
|
"blockId": "8XnDM1QsqPms4LQHh8q3Jo",
|
||||||
"type": "Webhook",
|
"type": "Webhook",
|
||||||
"options": { "responseVariableMapping": [], "variablesForTest": [] },
|
"options": { "responseVariableMapping": [], "variablesForTest": [] },
|
||||||
"webhook": {
|
"webhookId": "webhook1"
|
||||||
"id": "2L9mPYsLAXdXwcnGVK6pv9",
|
|
||||||
"method": "GET",
|
|
||||||
"headers": [],
|
|
||||||
"queryParams": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Block,
|
|
||||||
CredentialsType,
|
CredentialsType,
|
||||||
defaultSettings,
|
defaultSettings,
|
||||||
defaultTheme,
|
defaultTheme,
|
||||||
PublicBlock,
|
|
||||||
PublicTypebot,
|
PublicTypebot,
|
||||||
Step,
|
Step,
|
||||||
Typebot,
|
Typebot,
|
||||||
@@ -39,6 +37,9 @@ export const createUsers = () =>
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const createWebhook = (typebotId: string) =>
|
||||||
|
prisma.webhook.create({ data: { method: 'GET', typebotId, id: 'webhook1' } })
|
||||||
|
|
||||||
export const createCollaboration = (
|
export const createCollaboration = (
|
||||||
userId: string,
|
userId: string,
|
||||||
typebotId: string,
|
typebotId: string,
|
||||||
@@ -139,7 +140,7 @@ const parseTypebotToPublicTypebot = (
|
|||||||
): Omit<PublicTypebot, 'createdAt' | 'updatedAt'> => ({
|
): Omit<PublicTypebot, 'createdAt' | 'updatedAt'> => ({
|
||||||
id,
|
id,
|
||||||
name: typebot.name,
|
name: typebot.name,
|
||||||
blocks: parseBlocksToPublicBlocks(typebot.blocks),
|
blocks: typebot.blocks,
|
||||||
typebotId: typebot.id,
|
typebotId: typebot.id,
|
||||||
theme: typebot.theme,
|
theme: typebot.theme,
|
||||||
settings: typebot.settings,
|
settings: typebot.settings,
|
||||||
@@ -149,14 +150,6 @@ const parseTypebotToPublicTypebot = (
|
|||||||
customDomain: null,
|
customDomain: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const parseBlocksToPublicBlocks = (blocks: Block[]): PublicBlock[] =>
|
|
||||||
blocks.map((b) => ({
|
|
||||||
...b,
|
|
||||||
steps: b.steps.map((s) =>
|
|
||||||
'webhook' in s ? { ...s, webhook: s.webhook.id } : s
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const parseTestTypebot = (partialTypebot: Partial<Typebot>): Typebot => ({
|
const parseTestTypebot = (partialTypebot: Partial<Typebot>): Typebot => ({
|
||||||
id: partialTypebot.id ?? 'typebot',
|
id: partialTypebot.id ?? 'typebot',
|
||||||
folderId: null,
|
folderId: null,
|
||||||
@@ -232,6 +225,6 @@ export const importTypebotInDatabase = async (
|
|||||||
data: parseTypebotToPublicTypebot(
|
data: parseTypebotToPublicTypebot(
|
||||||
updates?.id ? `${updates?.id}-public` : 'publicBot',
|
updates?.id ? `${updates?.id}-public` : 'publicBot',
|
||||||
typebot
|
typebot
|
||||||
),
|
) as any,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import test, { expect, Page } from '@playwright/test'
|
import test, { expect, Page } from '@playwright/test'
|
||||||
import { importTypebotInDatabase } from '../../services/database'
|
import { createWebhook, importTypebotInDatabase } from '../../services/database'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { generate } from 'short-uuid'
|
import { generate } from 'short-uuid'
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ test.describe('Webhook step', () => {
|
|||||||
id: typebotId,
|
id: typebotId,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
await createWebhook(typebotId)
|
||||||
|
|
||||||
await page.goto(`/typebots/${typebotId}/edit`)
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
await page.click('text=Configure...')
|
await page.click('text=Configure...')
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import { Block, PublicBlock, PublicStep, PublicTypebot, Typebot } from 'models'
|
import { PublicTypebot, Typebot } from 'models'
|
||||||
import shortId from 'short-uuid'
|
import shortId from 'short-uuid'
|
||||||
import { HStack, Text } from '@chakra-ui/react'
|
import { HStack, Text } from '@chakra-ui/react'
|
||||||
import { CalendarIcon, CodeIcon } from 'assets/icons'
|
import { CalendarIcon, CodeIcon } from 'assets/icons'
|
||||||
import { StepIcon } from 'components/editor/StepsSideBar/StepIcon'
|
import { StepIcon } from 'components/editor/StepsSideBar/StepIcon'
|
||||||
import { byId, isInputStep, sendRequest } from 'utils'
|
import { byId, isInputStep, sendRequest } from 'utils'
|
||||||
import { isDefined } from '@udecode/plate-common'
|
|
||||||
|
|
||||||
export const parseTypebotToPublicTypebot = (
|
export const parseTypebotToPublicTypebot = (
|
||||||
typebot: Typebot
|
typebot: Typebot
|
||||||
): PublicTypebot => ({
|
): PublicTypebot => ({
|
||||||
id: shortId.generate(),
|
id: shortId.generate(),
|
||||||
typebotId: typebot.id,
|
typebotId: typebot.id,
|
||||||
blocks: parseBlocksToPublicBlocks(typebot.blocks),
|
blocks: typebot.blocks,
|
||||||
edges: typebot.edges,
|
edges: typebot.edges,
|
||||||
name: typebot.name,
|
name: typebot.name,
|
||||||
publicId: typebot.publicId,
|
publicId: typebot.publicId,
|
||||||
@@ -19,18 +18,29 @@ export const parseTypebotToPublicTypebot = (
|
|||||||
theme: typebot.theme,
|
theme: typebot.theme,
|
||||||
variables: typebot.variables,
|
variables: typebot.variables,
|
||||||
customDomain: typebot.customDomain,
|
customDomain: typebot.customDomain,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const parseBlocksToPublicBlocks = (blocks: Block[]): PublicBlock[] =>
|
export const parsePublicTypebotToTypebot = (
|
||||||
blocks.map((b) => ({
|
typebot: PublicTypebot,
|
||||||
...b,
|
existingTypebot: Typebot
|
||||||
steps: b.steps.map(
|
): Typebot => ({
|
||||||
(s) =>
|
id: typebot.typebotId,
|
||||||
('webhook' in s && isDefined(s.webhook)
|
blocks: typebot.blocks,
|
||||||
? { ...s, webhook: s.webhook.id }
|
edges: typebot.edges,
|
||||||
: s) as PublicStep
|
name: typebot.name,
|
||||||
),
|
publicId: typebot.publicId,
|
||||||
}))
|
settings: typebot.settings,
|
||||||
|
theme: typebot.theme,
|
||||||
|
variables: typebot.variables,
|
||||||
|
customDomain: typebot.customDomain,
|
||||||
|
createdAt: existingTypebot.createdAt,
|
||||||
|
updatedAt: existingTypebot.updatedAt,
|
||||||
|
publishedTypebotId: typebot.id,
|
||||||
|
folderId: existingTypebot.folderId,
|
||||||
|
ownerId: existingTypebot.ownerId,
|
||||||
|
})
|
||||||
|
|
||||||
export const createPublishedTypebot = async (typebot: PublicTypebot) =>
|
export const createPublishedTypebot = async (typebot: PublicTypebot) =>
|
||||||
sendRequest<PublicTypebot>({
|
sendRequest<PublicTypebot>({
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ import {
|
|||||||
defaultGoogleAnalyticsOptions,
|
defaultGoogleAnalyticsOptions,
|
||||||
defaultWebhookOptions,
|
defaultWebhookOptions,
|
||||||
StepWithOptionsType,
|
StepWithOptionsType,
|
||||||
defaultWebhookAttributes,
|
|
||||||
Webhook,
|
|
||||||
Item,
|
Item,
|
||||||
ItemType,
|
ItemType,
|
||||||
defaultConditionContent,
|
defaultConditionContent,
|
||||||
@@ -39,7 +37,7 @@ import {
|
|||||||
import shortId, { generate } from 'short-uuid'
|
import shortId, { generate } from 'short-uuid'
|
||||||
import { Typebot } from 'models'
|
import { Typebot } from 'models'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
import { fetcher, omit, toKebabCase } from '../utils'
|
import { fetcher, toKebabCase } from '../utils'
|
||||||
import {
|
import {
|
||||||
isBubbleStepType,
|
isBubbleStepType,
|
||||||
stepTypeHasItems,
|
stepTypeHasItems,
|
||||||
@@ -48,8 +46,8 @@ import {
|
|||||||
} from 'utils'
|
} from 'utils'
|
||||||
import { deepEqual } from 'fast-equals'
|
import { deepEqual } from 'fast-equals'
|
||||||
import { stringify } from 'qs'
|
import { stringify } from 'qs'
|
||||||
import { isChoiceInput, isConditionStep, sendRequest } from 'utils'
|
import { isChoiceInput, isConditionStep, sendRequest, omit } from 'utils'
|
||||||
import { parseBlocksToPublicBlocks } from '../publicTypebot'
|
import cuid from 'cuid'
|
||||||
|
|
||||||
export type TypebotInDashboard = Pick<
|
export type TypebotInDashboard = Pick<
|
||||||
Typebot,
|
Typebot,
|
||||||
@@ -173,16 +171,11 @@ export const parseNewStep = (
|
|||||||
options: stepTypeHasOption(type)
|
options: stepTypeHasOption(type)
|
||||||
? parseDefaultStepOptions(type)
|
? parseDefaultStepOptions(type)
|
||||||
: undefined,
|
: undefined,
|
||||||
webhook: stepTypeHasWebhook(type) ? parseDefaultWebhook() : undefined,
|
webhookId: stepTypeHasWebhook(type) ? cuid() : undefined,
|
||||||
items: stepTypeHasItems(type) ? parseDefaultItems(type, id) : undefined,
|
items: stepTypeHasItems(type) ? parseDefaultItems(type, id) : undefined,
|
||||||
} as DraggableStep
|
} as DraggableStep
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseDefaultWebhook = (): Webhook => ({
|
|
||||||
id: generate(),
|
|
||||||
...defaultWebhookAttributes,
|
|
||||||
})
|
|
||||||
|
|
||||||
const parseDefaultItems = (
|
const parseDefaultItems = (
|
||||||
type: LogicStepType.CONDITION | InputStepType.CHOICE,
|
type: LogicStepType.CONDITION | InputStepType.CHOICE,
|
||||||
stepId: string
|
stepId: string
|
||||||
@@ -255,7 +248,7 @@ export const checkIfPublished = (
|
|||||||
typebot: Typebot,
|
typebot: Typebot,
|
||||||
publicTypebot: PublicTypebot
|
publicTypebot: PublicTypebot
|
||||||
) =>
|
) =>
|
||||||
deepEqual(parseBlocksToPublicBlocks(typebot.blocks), publicTypebot.blocks) &&
|
deepEqual(typebot.blocks, publicTypebot.blocks) &&
|
||||||
deepEqual(typebot.settings, publicTypebot.settings) &&
|
deepEqual(typebot.settings, publicTypebot.settings) &&
|
||||||
deepEqual(typebot.theme, publicTypebot.theme) &&
|
deepEqual(typebot.theme, publicTypebot.theme) &&
|
||||||
deepEqual(typebot.variables, publicTypebot.variables)
|
deepEqual(typebot.variables, publicTypebot.variables)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import imageCompression from 'browser-image-compression'
|
import imageCompression from 'browser-image-compression'
|
||||||
import { Parser } from 'htmlparser2'
|
import { Parser } from 'htmlparser2'
|
||||||
import { PublicStep, Step, Typebot } from 'models'
|
import { Step, Typebot } from 'models'
|
||||||
|
|
||||||
export const fetcher = async (input: RequestInfo, init?: RequestInit) => {
|
export const fetcher = async (input: RequestInfo, init?: RequestInit) => {
|
||||||
const res = await fetch(input, init)
|
const res = await fetch(input, init)
|
||||||
@@ -36,26 +36,6 @@ export const toKebabCase = (value: string) => {
|
|||||||
return matched.map((x) => x.toLowerCase()).join('-')
|
return matched.map((x) => x.toLowerCase()).join('-')
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Omit {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
<T extends object, K extends [...(keyof T)[]]>(obj: T, ...keys: K): {
|
|
||||||
[K2 in Exclude<keyof T, K[number]>]: T[K2]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const omit: Omit = (obj, ...keys) => {
|
|
||||||
const ret = {} as {
|
|
||||||
[K in keyof typeof obj]: typeof obj[K]
|
|
||||||
}
|
|
||||||
let key: keyof typeof obj
|
|
||||||
for (key in obj) {
|
|
||||||
if (!keys.includes(key)) {
|
|
||||||
ret[key] = obj[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
export const uploadFile = async (file: File, key: string) => {
|
export const uploadFile = async (file: File, key: string) => {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`/api/storage/upload-url?key=${encodeURIComponent(
|
`/api/storage/upload-url?key=${encodeURIComponent(
|
||||||
@@ -98,7 +78,7 @@ export const removeUndefinedFields = <T>(obj: T): T =>
|
|||||||
{} as T
|
{} as T
|
||||||
)
|
)
|
||||||
|
|
||||||
export const stepHasOptions = (step: Step | PublicStep) => 'options' in step
|
export const stepHasOptions = (step: Step) => 'options' in step
|
||||||
|
|
||||||
export const parseVariableHighlight = (content: string, typebot: Typebot) => {
|
export const parseVariableHighlight = (content: string, typebot: Typebot) => {
|
||||||
const varNames = typebot.variables.map((v) => v.name)
|
const varNames = typebot.variables.map((v) => v.name)
|
||||||
@@ -128,3 +108,32 @@ export const readFile = (file: File): Promise<string> => {
|
|||||||
fr.readAsText(file)
|
fr.readAsText(file)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const timeSince = (date: string) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
const seconds = Math.floor((new Date() - new Date(date)) / 1000)
|
||||||
|
|
||||||
|
let interval = seconds / 31536000
|
||||||
|
|
||||||
|
if (interval > 1) {
|
||||||
|
return Math.floor(interval) + ' years'
|
||||||
|
}
|
||||||
|
interval = seconds / 2592000
|
||||||
|
if (interval > 1) {
|
||||||
|
return Math.floor(interval) + ' months'
|
||||||
|
}
|
||||||
|
interval = seconds / 86400
|
||||||
|
if (interval > 1) {
|
||||||
|
return Math.floor(interval) + ' days'
|
||||||
|
}
|
||||||
|
interval = seconds / 3600
|
||||||
|
if (interval > 1) {
|
||||||
|
return Math.floor(interval) + ' hours'
|
||||||
|
}
|
||||||
|
interval = seconds / 60
|
||||||
|
if (interval > 1) {
|
||||||
|
return Math.floor(interval) + ' minutes'
|
||||||
|
}
|
||||||
|
return Math.floor(seconds) + ' seconds'
|
||||||
|
}
|
||||||
|
|||||||
9
apps/builder/services/webhook.ts
Normal file
9
apps/builder/services/webhook.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Webhook } from 'models'
|
||||||
|
import { sendRequest } from 'utils'
|
||||||
|
|
||||||
|
export const saveWebhook = (webhookId: string, webhook: Partial<Webhook>) =>
|
||||||
|
sendRequest<{ webhook: Webhook }>({
|
||||||
|
method: 'PUT',
|
||||||
|
url: `/api/webhooks/${webhookId}`,
|
||||||
|
body: webhook,
|
||||||
|
})
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"test": "dotenv -e ./playwright/.env -e .env.local -- yarn playwright test",
|
"test": "yarn playwright test",
|
||||||
"test:open": "dotenv -e ./playwright/.env -e .env.local -v PWDEBUG=1 -- yarn playwright test"
|
"test:open": "PWDEBUG=1 yarn playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/nextjs": "^6.17.8",
|
"@sentry/nextjs": "^6.17.8",
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { NotFoundPage } from 'layouts/NotFoundPage'
|
import { NotFoundPage } from 'layouts/NotFoundPage'
|
||||||
import { PublicTypebot } from 'models'
|
import { PublicTypebot } from 'models'
|
||||||
import { GetServerSideProps, GetServerSidePropsContext } from 'next'
|
import { GetServerSideProps, GetServerSidePropsContext } from 'next'
|
||||||
|
import { isDefined, isNotDefined, omit } from 'utils'
|
||||||
import { TypebotPage, TypebotPageProps } from '../layouts/TypebotPage'
|
import { TypebotPage, TypebotPageProps } from '../layouts/TypebotPage'
|
||||||
import prisma from '../libs/prisma'
|
import prisma from '../libs/prisma'
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (
|
export const getServerSideProps: GetServerSideProps = async (
|
||||||
context: GetServerSidePropsContext
|
context: GetServerSidePropsContext
|
||||||
) => {
|
) => {
|
||||||
let typebot: PublicTypebot | null
|
let typebot: Omit<PublicTypebot, 'createdAt' | 'updatedAt'> | null
|
||||||
const isIE = /MSIE|Trident/.test(context.req.headers['user-agent'] ?? '')
|
const isIE = /MSIE|Trident/.test(context.req.headers['user-agent'] ?? '')
|
||||||
const pathname = context.resolvedUrl.split('?')[0]
|
const pathname = context.resolvedUrl.split('?')[0]
|
||||||
try {
|
try {
|
||||||
@@ -42,17 +43,23 @@ const getTypebotFromPublicId = async (publicId?: string) => {
|
|||||||
const typebot = await prisma.publicTypebot.findUnique({
|
const typebot = await prisma.publicTypebot.findUnique({
|
||||||
where: { publicId },
|
where: { publicId },
|
||||||
})
|
})
|
||||||
return (typebot as unknown as PublicTypebot) ?? null
|
if (isNotDefined(typebot)) return null
|
||||||
|
return omit(typebot as PublicTypebot, 'createdAt', 'updatedAt')
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTypebotFromCustomDomain = async (customDomain: string) => {
|
const getTypebotFromCustomDomain = async (customDomain: string) => {
|
||||||
const typebot = await prisma.publicTypebot.findUnique({
|
const typebot = await prisma.publicTypebot.findUnique({
|
||||||
where: { customDomain },
|
where: { customDomain },
|
||||||
})
|
})
|
||||||
return (typebot as unknown as PublicTypebot) ?? null
|
if (isNotDefined(typebot)) return null
|
||||||
|
return omit(typebot as PublicTypebot, 'createdAt', 'updatedAt')
|
||||||
}
|
}
|
||||||
|
|
||||||
const App = ({ typebot, ...props }: TypebotPageProps) =>
|
const App = ({ typebot, ...props }: TypebotPageProps) =>
|
||||||
typebot ? <TypebotPage typebot={typebot} {...props} /> : <NotFoundPage />
|
isDefined(typebot) ? (
|
||||||
|
<TypebotPage typebot={typebot} {...props} />
|
||||||
|
) : (
|
||||||
|
<NotFoundPage />
|
||||||
|
)
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Variable,
|
Variable,
|
||||||
Webhook,
|
Webhook,
|
||||||
WebhookResponse,
|
WebhookResponse,
|
||||||
|
WebhookStep,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import { parseVariables } from 'bot-engine'
|
import { parseVariables } from 'bot-engine'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
@@ -32,14 +33,16 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
}
|
}
|
||||||
const typebot = (await prisma.typebot.findUnique({
|
const typebot = (await prisma.typebot.findUnique({
|
||||||
where: { id: typebotId },
|
where: { id: typebotId },
|
||||||
})) as unknown as Typebot
|
include: { webhooks: true },
|
||||||
|
})) as unknown as Typebot & { webhooks: Webhook[] }
|
||||||
const step = typebot.blocks.find(byId(blockId))?.steps.find(byId(stepId))
|
const step = typebot.blocks.find(byId(blockId))?.steps.find(byId(stepId))
|
||||||
if (!step || !('webhook' in step))
|
const webhook = typebot.webhooks.find(byId((step as WebhookStep).webhookId))
|
||||||
|
if (!webhook)
|
||||||
return res
|
return res
|
||||||
.status(404)
|
.status(404)
|
||||||
.send({ statusCode: 404, data: { message: `Couldn't find webhook` } })
|
.send({ statusCode: 404, data: { message: `Couldn't find webhook` } })
|
||||||
const result = await executeWebhook(typebot)(
|
const result = await executeWebhook(typebot)(
|
||||||
step.webhook,
|
webhook,
|
||||||
variables,
|
variables,
|
||||||
blockId,
|
blockId,
|
||||||
resultValues
|
resultValues
|
||||||
@@ -133,7 +136,7 @@ const getBodyContent =
|
|||||||
resultValues,
|
resultValues,
|
||||||
blockId,
|
blockId,
|
||||||
}: {
|
}: {
|
||||||
body?: string
|
body?: string | null
|
||||||
resultValues?: ResultValues
|
resultValues?: ResultValues
|
||||||
blockId: string
|
blockId: string
|
||||||
}): string | undefined => {
|
}): string | undefined => {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { withSentry } from '@sentry/nextjs'
|
import { withSentry } from '@sentry/nextjs'
|
||||||
import { Prisma } from 'db'
|
|
||||||
import prisma from 'libs/prisma'
|
import prisma from 'libs/prisma'
|
||||||
import { HttpMethod, Typebot } from 'models'
|
import { Typebot, WebhookStep } from 'models'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { authenticateUser } from 'services/api/utils'
|
import { authenticateUser } from 'services/api/utils'
|
||||||
import { isWebhookStep, methodNotAllowed } from 'utils'
|
import { byId, methodNotAllowed } from 'utils'
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
@@ -15,17 +14,18 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
return res.status(403).send({ message: 'url is missing in body' })
|
return res.status(403).send({ message: 'url is missing in body' })
|
||||||
const { url } = body
|
const { url } = body
|
||||||
const typebotId = req.query.typebotId.toString()
|
const typebotId = req.query.typebotId.toString()
|
||||||
|
const blockId = req.query.blockId.toString()
|
||||||
const stepId = req.query.stepId.toString()
|
const stepId = req.query.stepId.toString()
|
||||||
const typebot = (await prisma.typebot.findUnique({
|
const typebot = (await prisma.typebot.findUnique({
|
||||||
where: { id_ownerId: { id: typebotId, ownerId: user.id } },
|
where: { id_ownerId: { id: typebotId, ownerId: user.id } },
|
||||||
})) as unknown as Typebot | undefined
|
})) as unknown as Typebot | undefined
|
||||||
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
|
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
|
||||||
try {
|
try {
|
||||||
const updatedTypebot = addUrlToWebhookStep(url, typebot, stepId)
|
const { webhookId } = typebot.blocks
|
||||||
await prisma.typebot.update({
|
.find(byId(blockId))
|
||||||
where: { id_ownerId: { id: typebotId, ownerId: user.id } },
|
?.steps.find(byId(stepId)) as WebhookStep
|
||||||
data: { blocks: updatedTypebot.blocks as Prisma.JsonArray },
|
await prisma.webhook.update({ where: { id: webhookId }, data: { url } })
|
||||||
})
|
|
||||||
return res.send({ message: 'success' })
|
return res.send({ message: 'success' })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return res
|
return res
|
||||||
@@ -36,30 +36,4 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
return methodNotAllowed(res)
|
return methodNotAllowed(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
const addUrlToWebhookStep = (
|
|
||||||
url: string,
|
|
||||||
typebot: Typebot,
|
|
||||||
stepId: string
|
|
||||||
): Typebot => ({
|
|
||||||
...typebot,
|
|
||||||
blocks: typebot.blocks.map((b) => ({
|
|
||||||
...b,
|
|
||||||
steps: b.steps.map((s) => {
|
|
||||||
if (s.id === stepId) {
|
|
||||||
if (!isWebhookStep(s)) throw new Error()
|
|
||||||
return {
|
|
||||||
...s,
|
|
||||||
webhook: {
|
|
||||||
...s.webhook,
|
|
||||||
url,
|
|
||||||
method: HttpMethod.POST,
|
|
||||||
body: '{{state}}',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
|
|
||||||
export default withSentry(handler)
|
export default withSentry(handler)
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
import { withSentry } from '@sentry/nextjs'
|
import { withSentry } from '@sentry/nextjs'
|
||||||
import { Prisma } from 'db'
|
|
||||||
import prisma from 'libs/prisma'
|
import prisma from 'libs/prisma'
|
||||||
import { Typebot } from 'models'
|
import { Typebot, WebhookStep } from 'models'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { authenticateUser } from 'services/api/utils'
|
import { authenticateUser } from 'services/api/utils'
|
||||||
import { omit } from 'services/utils'
|
import { byId, methodNotAllowed } from 'utils'
|
||||||
import { isWebhookStep, methodNotAllowed } from 'utils'
|
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const user = await authenticateUser(req)
|
const user = await authenticateUser(req)
|
||||||
if (!user) return res.status(401).json({ message: 'Not authenticated' })
|
if (!user) return res.status(401).json({ message: 'Not authenticated' })
|
||||||
const typebotId = req.query.typebotId.toString()
|
const typebotId = req.query.typebotId.toString()
|
||||||
|
const blockId = req.query.blockId.toString()
|
||||||
const stepId = req.query.stepId.toString()
|
const stepId = req.query.stepId.toString()
|
||||||
const typebot = (await prisma.typebot.findUnique({
|
const typebot = (await prisma.typebot.findUnique({
|
||||||
where: { id_ownerId: { id: typebotId, ownerId: user.id } },
|
where: { id_ownerId: { id: typebotId, ownerId: user.id } },
|
||||||
})) as unknown as Typebot | undefined
|
})) as unknown as Typebot | undefined
|
||||||
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
|
if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
|
||||||
try {
|
try {
|
||||||
const updatedTypebot = removeUrlFromWebhookStep(typebot, stepId)
|
const { webhookId } = typebot.blocks
|
||||||
await prisma.typebot.update({
|
.find(byId(blockId))
|
||||||
where: { id_ownerId: { id: typebotId, ownerId: user.id } },
|
?.steps.find(byId(stepId)) as WebhookStep
|
||||||
data: {
|
await prisma.webhook.update({
|
||||||
blocks: updatedTypebot.blocks as Prisma.JsonArray,
|
where: { id: webhookId },
|
||||||
},
|
data: { url: null },
|
||||||
})
|
})
|
||||||
|
|
||||||
return res.send({ message: 'success' })
|
return res.send({ message: 'success' })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return res
|
return res
|
||||||
@@ -35,21 +35,4 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
return methodNotAllowed(res)
|
return methodNotAllowed(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeUrlFromWebhookStep = (
|
|
||||||
typebot: Typebot,
|
|
||||||
stepId: string
|
|
||||||
): Typebot => ({
|
|
||||||
...typebot,
|
|
||||||
blocks: typebot.blocks.map((b) => ({
|
|
||||||
...b,
|
|
||||||
steps: b.steps.map((s) => {
|
|
||||||
if (s.id === stepId) {
|
|
||||||
if (!isWebhookStep(s)) throw new Error()
|
|
||||||
return { ...s, webhook: omit(s.webhook, 'url') }
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
|
|
||||||
export default withSentry(handler)
|
export default withSentry(handler)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import prisma from 'libs/prisma'
|
|||||||
import { Block } from 'models'
|
import { Block } from 'models'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { authenticateUser } from 'services/api/utils'
|
import { authenticateUser } from 'services/api/utils'
|
||||||
import { isWebhookStep, methodNotAllowed } from 'utils'
|
import { byId, isNotDefined, isWebhookStep, methodNotAllowed } from 'utils'
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
@@ -12,13 +12,15 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
const typebotId = req.query.typebotId.toString()
|
const typebotId = req.query.typebotId.toString()
|
||||||
const typebot = await prisma.typebot.findUnique({
|
const typebot = await prisma.typebot.findUnique({
|
||||||
where: { id_ownerId: { id: typebotId, ownerId: user.id } },
|
where: { id_ownerId: { id: typebotId, ownerId: user.id } },
|
||||||
select: { blocks: true },
|
select: { blocks: true, webhooks: true },
|
||||||
})
|
})
|
||||||
const emptyWebhookSteps = (typebot?.blocks as Block[]).reduce<
|
const emptyWebhookSteps = (typebot?.blocks as Block[]).reduce<
|
||||||
{ blockId: string; id: string; name: string }[]
|
{ blockId: string; id: string; name: string }[]
|
||||||
>((emptyWebhookSteps, block) => {
|
>((emptyWebhookSteps, block) => {
|
||||||
const steps = block.steps.filter(
|
const steps = block.steps.filter(
|
||||||
(step) => isWebhookStep(step) && !step.webhook.url
|
(step) =>
|
||||||
|
isWebhookStep(step) &&
|
||||||
|
isNotDefined(typebot?.webhooks.find(byId(step.webhookId))?.url)
|
||||||
)
|
)
|
||||||
return [
|
return [
|
||||||
...emptyWebhookSteps,
|
...emptyWebhookSteps,
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { devices, PlaywrightTestConfig } from '@playwright/test'
|
import { devices, PlaywrightTestConfig } from '@playwright/test'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
require('dotenv').config({ path: path.join(__dirname, '.env.local') })
|
||||||
|
|
||||||
const config: PlaywrightTestConfig = {
|
const config: PlaywrightTestConfig = {
|
||||||
globalSetup: require.resolve(path.join(__dirname, 'playwright/global-setup')),
|
globalSetup: require.resolve(path.join(__dirname, 'playwright/global-setup')),
|
||||||
testDir: path.join(__dirname, 'playwright/tests'),
|
testDir: path.join(__dirname, 'playwright/tests'),
|
||||||
|
|||||||
@@ -284,12 +284,7 @@
|
|||||||
"blockId": "webhookBlock",
|
"blockId": "webhookBlock",
|
||||||
"type": "Webhook",
|
"type": "Webhook",
|
||||||
"options": { "responseVariableMapping": [], "variablesForTest": [] },
|
"options": { "responseVariableMapping": [], "variablesForTest": [] },
|
||||||
"webhook": {
|
"webhookId": "webhook1"
|
||||||
"id": "3zZp4961n6CeorWR43jdV9",
|
|
||||||
"method": "GET",
|
|
||||||
"headers": [],
|
|
||||||
"queryParams": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { FullConfig } from '@playwright/test'
|
import { FullConfig } from '@playwright/test'
|
||||||
import { setupDatabase, teardownDatabase } from './services/database'
|
import { setupDatabase, teardownDatabase } from './services/database'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
require('dotenv').config({ path: '.env' })
|
|
||||||
|
|
||||||
async function globalSetup(config: FullConfig) {
|
async function globalSetup(config: FullConfig) {
|
||||||
const { baseURL } = config.projects[0].use
|
const { baseURL } = config.projects[0].use
|
||||||
if (!baseURL) throw new Error('baseURL is missing')
|
if (!baseURL) throw new Error('baseURL is missing')
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Block,
|
|
||||||
defaultSettings,
|
defaultSettings,
|
||||||
defaultTheme,
|
defaultTheme,
|
||||||
PublicBlock,
|
|
||||||
PublicTypebot,
|
PublicTypebot,
|
||||||
Step,
|
Step,
|
||||||
Typebot,
|
Typebot,
|
||||||
@@ -12,12 +10,13 @@ import { readFileSync } from 'fs'
|
|||||||
|
|
||||||
const prisma = new PrismaClient()
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
export const teardownDatabase = () => {
|
export const teardownDatabase = async () => {
|
||||||
try {
|
try {
|
||||||
return prisma.user.delete({
|
await prisma.user.delete({
|
||||||
where: { id: 'user' },
|
where: { id: 'user' },
|
||||||
})
|
})
|
||||||
} catch {}
|
} catch {}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setupDatabase = () => createUser()
|
export const setupDatabase = () => createUser()
|
||||||
@@ -32,6 +31,15 @@ export const createUser = () =>
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const createWebhook = (typebotId: string) =>
|
||||||
|
prisma.webhook.create({
|
||||||
|
data: {
|
||||||
|
id: 'webhook1',
|
||||||
|
typebotId: typebotId,
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export const createTypebots = async (partialTypebots: Partial<Typebot>[]) => {
|
export const createTypebots = async (partialTypebots: Partial<Typebot>[]) => {
|
||||||
await prisma.typebot.createMany({
|
await prisma.typebot.createMany({
|
||||||
data: partialTypebots.map(parseTestTypebot) as any[],
|
data: partialTypebots.map(parseTestTypebot) as any[],
|
||||||
@@ -49,7 +57,7 @@ const parseTypebotToPublicTypebot = (
|
|||||||
): PublicTypebot => ({
|
): PublicTypebot => ({
|
||||||
id,
|
id,
|
||||||
name: typebot.name,
|
name: typebot.name,
|
||||||
blocks: parseBlocksToPublicBlocks(typebot.blocks),
|
blocks: typebot.blocks,
|
||||||
typebotId: typebot.id,
|
typebotId: typebot.id,
|
||||||
theme: typebot.theme,
|
theme: typebot.theme,
|
||||||
settings: typebot.settings,
|
settings: typebot.settings,
|
||||||
@@ -57,16 +65,10 @@ const parseTypebotToPublicTypebot = (
|
|||||||
variables: typebot.variables,
|
variables: typebot.variables,
|
||||||
edges: typebot.edges,
|
edges: typebot.edges,
|
||||||
customDomain: null,
|
customDomain: null,
|
||||||
|
createdAt: typebot.createdAt,
|
||||||
|
updatedAt: typebot.updatedAt,
|
||||||
})
|
})
|
||||||
|
|
||||||
const parseBlocksToPublicBlocks = (blocks: Block[]): PublicBlock[] =>
|
|
||||||
blocks.map((b) => ({
|
|
||||||
...b,
|
|
||||||
steps: b.steps.map((s) =>
|
|
||||||
'webhook' in s ? { ...s, webhook: s.webhook.id } : s
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const parseTestTypebot = (partialTypebot: Partial<Typebot>): Typebot => ({
|
const parseTestTypebot = (partialTypebot: Partial<Typebot>): Typebot => ({
|
||||||
id: partialTypebot.id ?? 'typebot',
|
id: partialTypebot.id ?? 'typebot',
|
||||||
folderId: null,
|
folderId: null,
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import test, { expect } from '@playwright/test'
|
import test, { expect } from '@playwright/test'
|
||||||
import { createResults, importTypebotInDatabase } from '../services/database'
|
import {
|
||||||
|
createResults,
|
||||||
|
createWebhook,
|
||||||
|
importTypebotInDatabase,
|
||||||
|
} from '../services/database'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
const typebotId = 'webhook-flow'
|
const typebotId = 'webhook-flow'
|
||||||
@@ -9,6 +13,7 @@ test.beforeAll(async () => {
|
|||||||
path.join(__dirname, '../fixtures/typebots/api.json'),
|
path.join(__dirname, '../fixtures/typebots/api.json'),
|
||||||
{ id: typebotId }
|
{ id: typebotId }
|
||||||
)
|
)
|
||||||
|
await createWebhook(typebotId)
|
||||||
await createResults({ typebotId })
|
await createResults({ typebotId })
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
})
|
})
|
||||||
@@ -49,13 +54,13 @@ test('can get webhook steps', async ({ request }) => {
|
|||||||
test('can subscribe webhook', async ({ request }) => {
|
test('can subscribe webhook', async ({ request }) => {
|
||||||
expect(
|
expect(
|
||||||
(
|
(
|
||||||
await request.patch(
|
await request.post(
|
||||||
`/api/typebots/${typebotId}/blocks/webhookBlock/steps/webhookStep/subscribeWebhook`,
|
`/api/typebots/${typebotId}/blocks/webhookBlock/steps/webhookStep/subscribeWebhook`,
|
||||||
{ data: { url: 'https://test.com' } }
|
{ data: { url: 'https://test.com' } }
|
||||||
)
|
)
|
||||||
).status()
|
).status()
|
||||||
).toBe(401)
|
).toBe(401)
|
||||||
const response = await request.patch(
|
const response = await request.post(
|
||||||
`/api/typebots/${typebotId}/blocks/webhookBlock/steps/webhookStep/subscribeWebhook`,
|
`/api/typebots/${typebotId}/blocks/webhookBlock/steps/webhookStep/subscribeWebhook`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
@@ -73,12 +78,12 @@ test('can subscribe webhook', async ({ request }) => {
|
|||||||
test('can unsubscribe webhook', async ({ request }) => {
|
test('can unsubscribe webhook', async ({ request }) => {
|
||||||
expect(
|
expect(
|
||||||
(
|
(
|
||||||
await request.delete(
|
await request.post(
|
||||||
`/api/typebots/${typebotId}/blocks/webhookBlock/steps/webhookStep/unsubscribeWebhook`
|
`/api/typebots/${typebotId}/blocks/webhookBlock/steps/webhookStep/unsubscribeWebhook`
|
||||||
)
|
)
|
||||||
).status()
|
).status()
|
||||||
).toBe(401)
|
).toBe(401)
|
||||||
const response = await request.delete(
|
const response = await request.post(
|
||||||
`/api/typebots/${typebotId}/blocks/webhookBlock/steps/webhookStep/unsubscribeWebhook`,
|
`/api/typebots/${typebotId}/blocks/webhookBlock/steps/webhookStep/unsubscribeWebhook`,
|
||||||
{
|
{
|
||||||
headers: { Authorization: 'Bearer userToken' },
|
headers: { Authorization: 'Bearer userToken' },
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
interface Omit {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
<T extends object, K extends [...(keyof T)[]]>(obj: T, ...keys: K): {
|
|
||||||
[K2 in Exclude<keyof T, K[number]>]: T[K2]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const omit: Omit = (obj, ...keys) => {
|
|
||||||
const ret = {} as {
|
|
||||||
[K in keyof typeof obj]: typeof obj[K]
|
|
||||||
}
|
|
||||||
let key: keyof typeof obj
|
|
||||||
for (key in obj) {
|
|
||||||
if (!keys.includes(key)) {
|
|
||||||
ret[key] = obj[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group'
|
|||||||
import { ChatStep } from './ChatStep'
|
import { ChatStep } from './ChatStep'
|
||||||
import { AvatarSideContainer } from './AvatarSideContainer'
|
import { AvatarSideContainer } from './AvatarSideContainer'
|
||||||
import { HostAvatarsContext } from '../../contexts/HostAvatarsContext'
|
import { HostAvatarsContext } from '../../contexts/HostAvatarsContext'
|
||||||
import { PublicStep } from 'models'
|
|
||||||
import { useTypebot } from '../../contexts/TypebotContext'
|
import { useTypebot } from '../../contexts/TypebotContext'
|
||||||
import {
|
import {
|
||||||
isBubbleStep,
|
isBubbleStep,
|
||||||
@@ -17,9 +16,10 @@ import { executeIntegration } from 'services/integration'
|
|||||||
import { parseRetryStep, stepCanBeRetried } from 'services/inputs'
|
import { parseRetryStep, stepCanBeRetried } from 'services/inputs'
|
||||||
import { parseVariables } from 'index'
|
import { parseVariables } from 'index'
|
||||||
import { useAnswers } from 'contexts/AnswersContext'
|
import { useAnswers } from 'contexts/AnswersContext'
|
||||||
|
import { Step } from 'models'
|
||||||
|
|
||||||
type ChatBlockProps = {
|
type ChatBlockProps = {
|
||||||
steps: PublicStep[]
|
steps: Step[]
|
||||||
startStepIndex: number
|
startStepIndex: number
|
||||||
onScroll: () => void
|
onScroll: () => void
|
||||||
onBlockEnd: (edgeId?: string) => void
|
onBlockEnd: (edgeId?: string) => void
|
||||||
@@ -34,7 +34,7 @@ export const ChatBlock = ({
|
|||||||
const { typebot, updateVariableValue, createEdge, apiHost, isPreview } =
|
const { typebot, updateVariableValue, createEdge, apiHost, isPreview } =
|
||||||
useTypebot()
|
useTypebot()
|
||||||
const { resultValues } = useAnswers()
|
const { resultValues } = useAnswers()
|
||||||
const [displayedSteps, setDisplayedSteps] = useState<PublicStep[]>([])
|
const [displayedSteps, setDisplayedSteps] = useState<Step[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nextStep = steps[startStepIndex]
|
const nextStep = steps[startStepIndex]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useAnswers } from '../../../contexts/AnswersContext'
|
import { useAnswers } from '../../../contexts/AnswersContext'
|
||||||
import { useHostAvatars } from '../../../contexts/HostAvatarsContext'
|
import { useHostAvatars } from '../../../contexts/HostAvatarsContext'
|
||||||
import { InputStep, InputStepType, PublicStep } from 'models'
|
import { InputStep, InputStepType, Step } from 'models'
|
||||||
import { GuestBubble } from './bubbles/GuestBubble'
|
import { GuestBubble } from './bubbles/GuestBubble'
|
||||||
import { TextForm } from './inputs/TextForm'
|
import { TextForm } from './inputs/TextForm'
|
||||||
import { byId, isBubbleStep, isInputStep } from 'utils'
|
import { byId, isBubbleStep, isInputStep } from 'utils'
|
||||||
@@ -16,7 +16,7 @@ export const ChatStep = ({
|
|||||||
step,
|
step,
|
||||||
onTransitionEnd,
|
onTransitionEnd,
|
||||||
}: {
|
}: {
|
||||||
step: PublicStep
|
step: Step
|
||||||
onTransitionEnd: (answerContent?: string, isRetry?: boolean) => void
|
onTransitionEnd: (answerContent?: string, isRetry?: boolean) => void
|
||||||
}) => {
|
}) => {
|
||||||
const { addAnswer } = useAnswers()
|
const { addAnswer } = useAnswers()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useFrame } from 'react-frame-component'
|
|||||||
import { setCssVariablesValue } from '../services/theme'
|
import { setCssVariablesValue } from '../services/theme'
|
||||||
import { useAnswers } from '../contexts/AnswersContext'
|
import { useAnswers } from '../contexts/AnswersContext'
|
||||||
import { deepEqual } from 'fast-equals'
|
import { deepEqual } from 'fast-equals'
|
||||||
import { Answer, Edge, PublicBlock, Theme, VariableWithValue } from 'models'
|
import { Answer, Block, Edge, Theme, VariableWithValue } from 'models'
|
||||||
import { byId, isNotDefined } from 'utils'
|
import { byId, isNotDefined } from 'utils'
|
||||||
import { animateScroll as scroll } from 'react-scroll'
|
import { animateScroll as scroll } from 'react-scroll'
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
@@ -27,7 +27,7 @@ export const ConversationContainer = ({
|
|||||||
const { typebot, updateVariableValue } = useTypebot()
|
const { typebot, updateVariableValue } = useTypebot()
|
||||||
const { document: frameDocument } = useFrame()
|
const { document: frameDocument } = useFrame()
|
||||||
const [displayedBlocks, setDisplayedBlocks] = useState<
|
const [displayedBlocks, setDisplayedBlocks] = useState<
|
||||||
{ block: PublicBlock; startStepIndex: number }[]
|
{ block: Block; startStepIndex: number }[]
|
||||||
>([])
|
>([])
|
||||||
const [localAnswer, setLocalAnswer] = useState<Answer | undefined>()
|
const [localAnswer, setLocalAnswer] = useState<Answer | undefined>()
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
EmailInputStep,
|
EmailInputStep,
|
||||||
InputStepType,
|
InputStepType,
|
||||||
PhoneNumberInputStep,
|
PhoneNumberInputStep,
|
||||||
PublicStep,
|
Step,
|
||||||
UrlInputStep,
|
UrlInputStep,
|
||||||
Variable,
|
Variable,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
@@ -34,7 +34,7 @@ export const isInputValid = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const stepCanBeRetried = (
|
export const stepCanBeRetried = (
|
||||||
step: PublicStep
|
step: Step
|
||||||
): step is EmailInputStep | UrlInputStep | PhoneNumberInputStep =>
|
): step is EmailInputStep | UrlInputStep | PhoneNumberInputStep =>
|
||||||
isInputStep(step) && 'retryMessageContent' in step.options
|
isInputStep(step) && 'retryMessageContent' in step.options
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import {
|
|||||||
GoogleAnalyticsStep,
|
GoogleAnalyticsStep,
|
||||||
WebhookStep,
|
WebhookStep,
|
||||||
SendEmailStep,
|
SendEmailStep,
|
||||||
PublicBlock,
|
|
||||||
ZapierStep,
|
ZapierStep,
|
||||||
ResultValues,
|
ResultValues,
|
||||||
|
Block,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import { stringify } from 'qs'
|
import { stringify } from 'qs'
|
||||||
import { sendRequest } from 'utils'
|
import { sendRequest } from 'utils'
|
||||||
@@ -31,7 +31,7 @@ type IntegrationContext = {
|
|||||||
isPreview: boolean
|
isPreview: boolean
|
||||||
variables: Variable[]
|
variables: Variable[]
|
||||||
resultValues: ResultValues
|
resultValues: ResultValues
|
||||||
blocks: PublicBlock[]
|
blocks: Block[]
|
||||||
updateVariableValue: (variableId: string, value: string) => void
|
updateVariableValue: (variableId: string, value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +170,6 @@ const executeWebhook = async (
|
|||||||
isPreview,
|
isPreview,
|
||||||
}: IntegrationContext
|
}: IntegrationContext
|
||||||
) => {
|
) => {
|
||||||
if (!step.webhook) return step.outgoingEdgeId
|
|
||||||
const { data, error } = await sendRequest({
|
const { data, error } = await sendRequest({
|
||||||
url: `${apiHost}/api/typebots/${typebotId}/blocks/${blockId}/steps/${stepId}/executeWebhook`,
|
url: `${apiHost}/api/typebots/${typebotId}/blocks/${blockId}/steps/${stepId}/executeWebhook`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -186,6 +185,7 @@ const executeWebhook = async (
|
|||||||
const value = safeEval(`(${JSON.stringify(data)}).${varMapping?.bodyPath}`)
|
const value = safeEval(`(${JSON.stringify(data)}).${varMapping?.bodyPath}`)
|
||||||
updateVariableValue(varMapping.variableId, value)
|
updateVariableValue(varMapping.variableId, value)
|
||||||
})
|
})
|
||||||
|
return step.outgoingEdgeId
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendEmail = async (
|
const sendEmail = async (
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Webhook" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"url" TEXT,
|
||||||
|
"method" TEXT NOT NULL,
|
||||||
|
"queryParams" JSONB[],
|
||||||
|
"headers" JSONB[],
|
||||||
|
"body" TEXT,
|
||||||
|
"typebotId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Webhook_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Webhook" ADD CONSTRAINT "Webhook_typebotId_fkey" FOREIGN KEY ("typebotId") REFERENCES "Typebot"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -129,6 +129,7 @@ model Typebot {
|
|||||||
customDomain String? @unique
|
customDomain String? @unique
|
||||||
collaborators CollaboratorsOnTypebots[]
|
collaborators CollaboratorsOnTypebots[]
|
||||||
invitations Invitation[]
|
invitations Invitation[]
|
||||||
|
webhooks Webhook[]
|
||||||
|
|
||||||
@@unique([id, ownerId])
|
@@unique([id, ownerId])
|
||||||
}
|
}
|
||||||
@@ -202,3 +203,14 @@ model Coupon {
|
|||||||
code String @id @unique
|
code String @id @unique
|
||||||
dateRedeemed DateTime?
|
dateRedeemed DateTime?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Webhook {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
url String?
|
||||||
|
method String
|
||||||
|
queryParams Json[]
|
||||||
|
headers Json[]
|
||||||
|
body String?
|
||||||
|
typebotId String
|
||||||
|
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ export * from './result'
|
|||||||
export * from './answer'
|
export * from './answer'
|
||||||
export * from './utils'
|
export * from './utils'
|
||||||
export * from './credentials'
|
export * from './credentials'
|
||||||
|
export * from './webhooks'
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
import { Block, Edge, Settings, Step, Theme, Variable } from './typebot'
|
import { Block, Edge, Settings, Theme, Variable } from './typebot'
|
||||||
import { PublicTypebot as PublicTypebotFromPrisma } from 'db'
|
import { PublicTypebot as PublicTypebotFromPrisma } from 'db'
|
||||||
|
|
||||||
export type PublicTypebot = Omit<
|
export type PublicTypebot = Omit<
|
||||||
PublicTypebotFromPrisma,
|
PublicTypebotFromPrisma,
|
||||||
| 'blocks'
|
'blocks' | 'theme' | 'settings' | 'variables' | 'edges'
|
||||||
| 'theme'
|
|
||||||
| 'settings'
|
|
||||||
| 'variables'
|
|
||||||
| 'edges'
|
|
||||||
| 'createdAt'
|
|
||||||
| 'updatedAt'
|
|
||||||
> & {
|
> & {
|
||||||
blocks: PublicBlock[]
|
blocks: Block[]
|
||||||
variables: Variable[]
|
variables: Variable[]
|
||||||
edges: Edge[]
|
edges: Edge[]
|
||||||
theme: Theme
|
theme: Theme
|
||||||
settings: Settings
|
settings: Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PublicBlock = Omit<Block, 'steps'> & { steps: PublicStep[] }
|
|
||||||
export type PublicStep = Omit<Step, 'webhook'> & { webhook?: string }
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export type GoogleAnalyticsStep = StepBase & {
|
|||||||
export type WebhookStep = StepBase & {
|
export type WebhookStep = StepBase & {
|
||||||
type: IntegrationStepType.WEBHOOK
|
type: IntegrationStepType.WEBHOOK
|
||||||
options: WebhookOptions
|
options: WebhookOptions
|
||||||
webhook: Webhook
|
webhookId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ZapierStep = Omit<WebhookStep, 'type'> & {
|
export type ZapierStep = Omit<WebhookStep, 'type'> & {
|
||||||
@@ -118,39 +118,12 @@ export type WebhookOptions = {
|
|||||||
responseVariableMapping: ResponseVariableMapping[]
|
responseVariableMapping: ResponseVariableMapping[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum HttpMethod {
|
|
||||||
POST = 'POST',
|
|
||||||
GET = 'GET',
|
|
||||||
PUT = 'PUT',
|
|
||||||
DELETE = 'DELETE',
|
|
||||||
PATCH = 'PATCH',
|
|
||||||
HEAD = 'HEAD',
|
|
||||||
CONNECT = 'CONNECT',
|
|
||||||
OPTIONS = 'OPTIONS',
|
|
||||||
TRACE = 'TRACE',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type KeyValue = { id: string; key?: string; value?: string }
|
|
||||||
export type VariableForTest = {
|
export type VariableForTest = {
|
||||||
id: string
|
id: string
|
||||||
variableId?: string
|
variableId?: string
|
||||||
value?: string
|
value?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Webhook = {
|
|
||||||
id: string
|
|
||||||
url?: string
|
|
||||||
method: HttpMethod
|
|
||||||
queryParams: KeyValue[]
|
|
||||||
headers: KeyValue[]
|
|
||||||
body?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WebhookResponse = {
|
|
||||||
statusCode: number
|
|
||||||
data?: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultGoogleSheetsOptions: GoogleSheetsOptions = {}
|
export const defaultGoogleSheetsOptions: GoogleSheetsOptions = {}
|
||||||
|
|
||||||
export const defaultGoogleAnalyticsOptions: GoogleAnalyticsOptions = {}
|
export const defaultGoogleAnalyticsOptions: GoogleAnalyticsOptions = {}
|
||||||
@@ -160,12 +133,6 @@ export const defaultWebhookOptions: Omit<WebhookOptions, 'webhookId'> = {
|
|||||||
variablesForTest: [],
|
variablesForTest: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultWebhookAttributes: Omit<Webhook, 'id'> = {
|
|
||||||
method: HttpMethod.GET,
|
|
||||||
headers: [],
|
|
||||||
queryParams: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultSendEmailOptions: SendEmailOptions = {
|
export const defaultSendEmailOptions: SendEmailOptions = {
|
||||||
credentialsId: 'default',
|
credentialsId: 'default',
|
||||||
recipients: [],
|
recipients: [],
|
||||||
|
|||||||
38
packages/models/src/webhooks.ts
Normal file
38
packages/models/src/webhooks.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Webhook as WebhookFromPrisma } from 'db'
|
||||||
|
|
||||||
|
export enum HttpMethod {
|
||||||
|
POST = 'POST',
|
||||||
|
GET = 'GET',
|
||||||
|
PUT = 'PUT',
|
||||||
|
DELETE = 'DELETE',
|
||||||
|
PATCH = 'PATCH',
|
||||||
|
HEAD = 'HEAD',
|
||||||
|
CONNECT = 'CONNECT',
|
||||||
|
OPTIONS = 'OPTIONS',
|
||||||
|
TRACE = 'TRACE',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type KeyValue = { id: string; key?: string; value?: string }
|
||||||
|
|
||||||
|
export type Webhook = Omit<
|
||||||
|
WebhookFromPrisma,
|
||||||
|
'queryParams' | 'headers' | 'method'
|
||||||
|
> & {
|
||||||
|
queryParams: KeyValue[]
|
||||||
|
headers: KeyValue[]
|
||||||
|
method: HttpMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WebhookResponse = {
|
||||||
|
statusCode: number
|
||||||
|
data?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultWebhookAttributes: Omit<
|
||||||
|
Webhook,
|
||||||
|
'id' | 'body' | 'url' | 'typebotId'
|
||||||
|
> = {
|
||||||
|
method: HttpMethod.GET,
|
||||||
|
headers: [],
|
||||||
|
queryParams: [],
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"db": "*",
|
"db": "*",
|
||||||
|
"models": "*",
|
||||||
|
"utils": "*",
|
||||||
"ts-node": "^10.5.0"
|
"ts-node": "^10.5.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
Block,
|
Block,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { byId, isDefined } from '.'
|
import { byId, isDefined } from './utils'
|
||||||
|
|
||||||
export const methodNotAllowed = (res: NextApiResponse) =>
|
export const methodNotAllowed = (res: NextApiResponse) =>
|
||||||
res.status(405).json({ message: 'Method Not Allowed' })
|
res.status(405).json({ message: 'Method Not Allowed' })
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
WebhookStep,
|
WebhookStep,
|
||||||
StepType,
|
StepType,
|
||||||
StepWithOptionsType,
|
StepWithOptionsType,
|
||||||
PublicStep,
|
|
||||||
ImageBubbleStep,
|
ImageBubbleStep,
|
||||||
VideoBubbleStep,
|
VideoBubbleStep,
|
||||||
} from 'models'
|
} from 'models'
|
||||||
@@ -62,50 +61,42 @@ export const isNotDefined = <T>(
|
|||||||
value: T | undefined | null
|
value: T | undefined | null
|
||||||
): value is undefined | null => value === undefined || value === null
|
): value is undefined | null => value === undefined || value === null
|
||||||
|
|
||||||
export const isInputStep = (step: Step | PublicStep): step is InputStep =>
|
export const isInputStep = (step: Step): step is InputStep =>
|
||||||
(Object.values(InputStepType) as string[]).includes(step.type)
|
(Object.values(InputStepType) as string[]).includes(step.type)
|
||||||
|
|
||||||
export const isBubbleStep = (step: Step | PublicStep): step is BubbleStep =>
|
export const isBubbleStep = (step: Step): step is BubbleStep =>
|
||||||
(Object.values(BubbleStepType) as string[]).includes(step.type)
|
(Object.values(BubbleStepType) as string[]).includes(step.type)
|
||||||
|
|
||||||
export const isLogicStep = (step: Step | PublicStep): step is LogicStep =>
|
export const isLogicStep = (step: Step): step is LogicStep =>
|
||||||
(Object.values(LogicStepType) as string[]).includes(step.type)
|
(Object.values(LogicStepType) as string[]).includes(step.type)
|
||||||
|
|
||||||
export const isTextBubbleStep = (
|
export const isTextBubbleStep = (step: Step): step is TextBubbleStep =>
|
||||||
step: Step | PublicStep
|
step.type === BubbleStepType.TEXT
|
||||||
): step is TextBubbleStep => step.type === BubbleStepType.TEXT
|
|
||||||
|
|
||||||
export const isMediaBubbleStep = (
|
export const isMediaBubbleStep = (
|
||||||
step: Step | PublicStep
|
step: Step
|
||||||
): step is ImageBubbleStep | VideoBubbleStep =>
|
): step is ImageBubbleStep | VideoBubbleStep =>
|
||||||
step.type === BubbleStepType.IMAGE || step.type === BubbleStepType.VIDEO
|
step.type === BubbleStepType.IMAGE || step.type === BubbleStepType.VIDEO
|
||||||
|
|
||||||
export const isTextInputStep = (
|
export const isTextInputStep = (step: Step): step is TextInputStep =>
|
||||||
step: Step | PublicStep
|
step.type === InputStepType.TEXT
|
||||||
): step is TextInputStep => step.type === InputStepType.TEXT
|
|
||||||
|
|
||||||
export const isChoiceInput = (
|
export const isChoiceInput = (step: Step): step is ChoiceInputStep =>
|
||||||
step: Step | PublicStep
|
step.type === InputStepType.CHOICE
|
||||||
): step is ChoiceInputStep => step.type === InputStepType.CHOICE
|
|
||||||
|
|
||||||
export const isSingleChoiceInput = (
|
export const isSingleChoiceInput = (step: Step): step is ChoiceInputStep =>
|
||||||
step: Step | PublicStep
|
|
||||||
): step is ChoiceInputStep =>
|
|
||||||
step.type === InputStepType.CHOICE &&
|
step.type === InputStepType.CHOICE &&
|
||||||
'options' in step &&
|
'options' in step &&
|
||||||
!step.options.isMultipleChoice
|
!step.options.isMultipleChoice
|
||||||
|
|
||||||
export const isConditionStep = (
|
export const isConditionStep = (step: Step): step is ConditionStep =>
|
||||||
step: Step | PublicStep
|
step.type === LogicStepType.CONDITION
|
||||||
): step is ConditionStep => step.type === LogicStepType.CONDITION
|
|
||||||
|
|
||||||
export const isIntegrationStep = (
|
export const isIntegrationStep = (step: Step): step is IntegrationStep =>
|
||||||
step: Step | PublicStep
|
|
||||||
): step is IntegrationStep =>
|
|
||||||
(Object.values(IntegrationStepType) as string[]).includes(step.type)
|
(Object.values(IntegrationStepType) as string[]).includes(step.type)
|
||||||
|
|
||||||
export const isWebhookStep = (step: Step | PublicStep): step is WebhookStep =>
|
export const isWebhookStep = (step: Step): step is WebhookStep =>
|
||||||
'webhook' in step
|
'webhookId' in step
|
||||||
|
|
||||||
export const isBubbleStepType = (type: StepType): type is BubbleStepType =>
|
export const isBubbleStepType = (type: StepType): type is BubbleStepType =>
|
||||||
(Object.values(BubbleStepType) as string[]).includes(type)
|
(Object.values(BubbleStepType) as string[]).includes(type)
|
||||||
@@ -132,9 +123,29 @@ export const stepTypeHasItems = (
|
|||||||
type === LogicStepType.CONDITION || type === InputStepType.CHOICE
|
type === LogicStepType.CONDITION || type === InputStepType.CHOICE
|
||||||
|
|
||||||
export const stepHasItems = (
|
export const stepHasItems = (
|
||||||
step: Step | PublicStep
|
step: Step
|
||||||
): step is ConditionStep | ChoiceInputStep => 'items' in step
|
): step is ConditionStep | ChoiceInputStep => 'items' in step
|
||||||
|
|
||||||
export const byId = (id?: string) => (obj: { id: string }) => obj.id === id
|
export const byId = (id?: string) => (obj: { id: string }) => obj.id === id
|
||||||
|
|
||||||
export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1)
|
export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1)
|
||||||
|
|
||||||
|
interface Omit {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
<T extends object, K extends [...(keyof T)[]]>(obj: T, ...keys: K): {
|
||||||
|
[K2 in Exclude<keyof T, K[number]>]: T[K2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const omit: Omit = (obj, ...keys) => {
|
||||||
|
const ret = {} as {
|
||||||
|
[K in keyof typeof obj]: typeof obj[K]
|
||||||
|
}
|
||||||
|
let key: keyof typeof obj
|
||||||
|
for (key in obj) {
|
||||||
|
if (!keys.includes(key)) {
|
||||||
|
ret[key] = obj[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user