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,
|
||||
TextBubbleStep,
|
||||
Webhook,
|
||||
WebhookStep,
|
||||
} from 'models'
|
||||
import { useRef } from 'react'
|
||||
import {
|
||||
@@ -95,11 +94,6 @@ export const StepSettings = ({
|
||||
const handleOptionsChange = (options: StepOptions) => {
|
||||
onStepChange({ options } as Partial<Step>)
|
||||
}
|
||||
const handleWebhookChange = (updates: Partial<Webhook>) => {
|
||||
onStepChange({
|
||||
webhook: { ...(step as WebhookStep).webhook, ...updates },
|
||||
} as Partial<Step>)
|
||||
}
|
||||
const handleItemChange = (updates: Partial<ConditionItem>) => {
|
||||
onStepChange({
|
||||
items: [{ ...(step as ConditionStep).items[0], ...updates }],
|
||||
@@ -208,7 +202,6 @@ export const StepSettings = ({
|
||||
<WebhookSettings
|
||||
step={step}
|
||||
onOptionsChange={handleOptionsChange}
|
||||
onWebhookChange={handleWebhookChange}
|
||||
onTestRequestClick={onTestRequestClick}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -18,12 +18,11 @@ import {
|
||||
Sheet,
|
||||
useSheets,
|
||||
} from 'services/integrations'
|
||||
import { isDefined } from 'utils'
|
||||
import { isDefined, omit } from 'utils'
|
||||
import { SheetsDropdown } from './SheetsDropdown'
|
||||
import { SpreadsheetsDropdown } from './SpreadsheetDropdown'
|
||||
import { CellWithValueStack } from './CellWithValueStack'
|
||||
import { CellWithVariableIdStack } from './CellWithVariableIdStack'
|
||||
import { omit } from 'services/utils'
|
||||
import { CredentialsDropdown } from 'components/shared/CredentialsDropdown'
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
AccordionPanel,
|
||||
Button,
|
||||
Flex,
|
||||
Spinner,
|
||||
Stack,
|
||||
useToast,
|
||||
} from '@chakra-ui/react'
|
||||
@@ -17,9 +18,10 @@ import {
|
||||
KeyValue,
|
||||
WebhookOptions,
|
||||
VariableForTest,
|
||||
Webhook,
|
||||
ResponseVariableMapping,
|
||||
WebhookStep,
|
||||
defaultWebhookAttributes,
|
||||
Webhook,
|
||||
} from 'models'
|
||||
import { DropdownList } from 'components/shared/DropdownList'
|
||||
import { TableList, TableListItemProps } from 'components/shared/TableList'
|
||||
@@ -32,21 +34,21 @@ import {
|
||||
import { HeadersInputs, QueryParamsInputs } from './KeyValueInputs'
|
||||
import { VariableForTestInputs } from './VariableForTestInputs'
|
||||
import { DataVariableInputs } from './ResponseMappingInputs'
|
||||
import { byId } from 'utils'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
type Props = {
|
||||
step: WebhookStep
|
||||
onOptionsChange: (options: WebhookOptions) => void
|
||||
onWebhookChange: (updates: Partial<Webhook>) => void
|
||||
onTestRequestClick: () => void
|
||||
}
|
||||
|
||||
export const WebhookSettings = ({
|
||||
step: { webhook, options, blockId, id: stepId },
|
||||
step: { options, blockId, id: stepId, webhookId },
|
||||
onOptionsChange,
|
||||
onWebhookChange,
|
||||
onTestRequestClick,
|
||||
}: Props) => {
|
||||
const { typebot, save } = useTypebot()
|
||||
const { typebot, save, webhooks, updateWebhook } = useTypebot()
|
||||
const [isTestResponseLoading, setIsTestResponseLoading] = useState(false)
|
||||
const [testResponse, setTestResponse] = useState<string>()
|
||||
const [responseKeys, setResponseKeys] = useState<string[]>([])
|
||||
@@ -55,18 +57,52 @@ export const WebhookSettings = ({
|
||||
position: 'top-right',
|
||||
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[]) =>
|
||||
onWebhookChange({ queryParams })
|
||||
localWebhook && setLocalWebhook({ ...localWebhook, queryParams })
|
||||
|
||||
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[]) =>
|
||||
onOptionsChange({ ...options, variablesForTest })
|
||||
@@ -76,10 +112,10 @@ export const WebhookSettings = ({
|
||||
) => onOptionsChange({ ...options, responseVariableMapping })
|
||||
|
||||
const handleTestRequestClick = async () => {
|
||||
if (!typebot) return
|
||||
if (!typebot || !localWebhook) return
|
||||
setIsTestResponseLoading(true)
|
||||
onTestRequestClick()
|
||||
await save()
|
||||
await Promise.all([updateWebhook(localWebhook.id, localWebhook), save()])
|
||||
const { data, error } = await executeWebhook(
|
||||
typebot.id,
|
||||
convertVariableForTestToVariables(
|
||||
@@ -100,19 +136,20 @@ export const WebhookSettings = ({
|
||||
[responseKeys]
|
||||
)
|
||||
|
||||
if (!localWebhook) return <Spinner />
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Stack>
|
||||
<Flex>
|
||||
<DropdownList<HttpMethod>
|
||||
currentItem={webhook.method}
|
||||
currentItem={localWebhook.method as HttpMethod}
|
||||
onItemSelect={handleMethodChange}
|
||||
items={Object.values(HttpMethod)}
|
||||
/>
|
||||
</Flex>
|
||||
<InputWithVariableButton
|
||||
placeholder="Your Webhook URL..."
|
||||
initialValue={webhook.url ?? ''}
|
||||
initialValue={localWebhook.url ?? ''}
|
||||
onChange={handleUrlChange}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -124,7 +161,7 @@ export const WebhookSettings = ({
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<KeyValue>
|
||||
initialItems={webhook.queryParams}
|
||||
initialItems={localWebhook.queryParams}
|
||||
onItemsChange={handleQueryParamsChange}
|
||||
Item={QueryParamsInputs}
|
||||
addLabel="Add a param"
|
||||
@@ -138,7 +175,7 @@ export const WebhookSettings = ({
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<TableList<KeyValue>
|
||||
initialItems={webhook.headers}
|
||||
initialItems={localWebhook.headers}
|
||||
onItemsChange={handleHeadersChange}
|
||||
Item={HeadersInputs}
|
||||
addLabel="Add a value"
|
||||
@@ -152,7 +189,7 @@ export const WebhookSettings = ({
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4} as={Stack} spacing="6">
|
||||
<CodeEditor
|
||||
value={webhook.body ?? ''}
|
||||
value={localWebhook.body ?? ''}
|
||||
lang="json"
|
||||
onChange={handleBodyChange}
|
||||
/>
|
||||
|
||||
@@ -8,23 +8,27 @@ import {
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { ExternalLinkIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { ZapierStep } from 'models'
|
||||
import React from 'react'
|
||||
import { byId } from 'utils'
|
||||
|
||||
type Props = {
|
||||
step: ZapierStep
|
||||
}
|
||||
|
||||
export const ZapierSettings = ({ step }: Props) => {
|
||||
const { webhooks } = useTypebot()
|
||||
const webhook = webhooks.find(byId(step.webhookId))
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Alert
|
||||
status={step.webhook.url ? 'success' : 'info'}
|
||||
bgColor={step.webhook.url ? undefined : 'blue.50'}
|
||||
status={webhook?.url ? 'success' : 'info'}
|
||||
bgColor={webhook?.url ? undefined : 'blue.50'}
|
||||
rounded="md"
|
||||
>
|
||||
<AlertIcon />
|
||||
{step.webhook.url ? (
|
||||
{webhook?.url ? (
|
||||
<>Your zap is correctly configured 🚀</>
|
||||
) : (
|
||||
<Stack>
|
||||
@@ -40,7 +44,7 @@ export const ZapierSettings = ({ step }: Props) => {
|
||||
</Stack>
|
||||
)}
|
||||
</Alert>
|
||||
{step.webhook.url && <Input value={step.webhook.url} isDisabled />}
|
||||
{webhook?.url && <Input value={webhook?.url} isDisabled />}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { WebhookStep } from 'models'
|
||||
import { byId } from 'utils'
|
||||
|
||||
type Props = {
|
||||
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>
|
||||
return (
|
||||
<Text isTruncated pr="6">
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { ZapierStep } from 'models'
|
||||
import { isNotDefined } from 'utils'
|
||||
import { byId, isNotDefined } from 'utils'
|
||||
|
||||
type Props = {
|
||||
step: ZapierStep
|
||||
}
|
||||
|
||||
export const ZapierContent = ({ step }: Props) => {
|
||||
if (isNotDefined(step.webhook.body))
|
||||
export const ZapierContent = ({ step: { webhookId } }: Props) => {
|
||||
const { webhooks } = useTypebot()
|
||||
const webhook = webhooks.find(byId(webhookId))
|
||||
|
||||
if (isNotDefined(webhook?.body))
|
||||
return <Text color="gray.500">Configure...</Text>
|
||||
return (
|
||||
<Text isTruncated pr="6">
|
||||
{step.webhook.url ? 'Enabled' : 'Disabled'}
|
||||
{webhook?.url ? 'Enabled' : 'Disabled'}
|
||||
</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 { timeSince } from 'services/utils'
|
||||
import { isNotDefined } from 'utils'
|
||||
|
||||
export const PublishButton = () => {
|
||||
const { isPublishing, isPublished, publishTypebot } = useTypebot()
|
||||
const {
|
||||
isPublishing,
|
||||
isPublished,
|
||||
publishTypebot,
|
||||
publishedTypebot,
|
||||
restorePublishedTypebot,
|
||||
} = useTypebot()
|
||||
|
||||
return (
|
||||
<Button
|
||||
ml={2}
|
||||
colorScheme="blue"
|
||||
isLoading={isPublishing}
|
||||
isDisabled={isPublished}
|
||||
onClick={publishTypebot}
|
||||
>
|
||||
{isPublished ? 'Published' : 'Publish'}
|
||||
</Button>
|
||||
<HStack spacing="1px">
|
||||
<Tooltip
|
||||
borderRadius="md"
|
||||
hasArrow
|
||||
placement="bottom-end"
|
||||
label={
|
||||
<Stack>
|
||||
<Text>There are non published changes.</Text>
|
||||
<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 {
|
||||
createContext,
|
||||
Dispatch,
|
||||
@@ -88,7 +88,7 @@ export const GraphProvider = ({
|
||||
isReadOnly = false,
|
||||
}: {
|
||||
children: ReactNode
|
||||
blocks: (Block | PublicBlock)[]
|
||||
blocks: Block[]
|
||||
isReadOnly?: boolean
|
||||
}) => {
|
||||
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 {
|
||||
createContext,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from 'react'
|
||||
import {
|
||||
createPublishedTypebot,
|
||||
parsePublicTypebotToTypebot,
|
||||
parseTypebotToPublicTypebot,
|
||||
updatePublishedTypebot,
|
||||
} from 'services/publicTypebot'
|
||||
@@ -34,6 +35,7 @@ import { itemsAction, ItemsActions } from './actions/items'
|
||||
import { generate } from 'short-uuid'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { User } from 'db'
|
||||
import { saveWebhook } from 'services/webhook'
|
||||
const autoSaveTimeout = 10000
|
||||
|
||||
type UpdateTypebotPayload = Partial<{
|
||||
@@ -50,6 +52,7 @@ const typebotContext = createContext<
|
||||
typebot?: Typebot
|
||||
publishedTypebot?: PublicTypebot
|
||||
owner?: User
|
||||
webhooks: Webhook[]
|
||||
isReadOnly?: boolean
|
||||
isPublished: boolean
|
||||
isPublishing: boolean
|
||||
@@ -59,6 +62,10 @@ const typebotContext = createContext<
|
||||
redo: () => void
|
||||
canRedo: boolean
|
||||
canUndo: boolean
|
||||
updateWebhook: (
|
||||
webhookId: string,
|
||||
webhook: Partial<Webhook>
|
||||
) => Promise<void>
|
||||
updateTypebot: (updates: UpdateTypebotPayload) => void
|
||||
updateOnBothTypebots: (updates: {
|
||||
publicId?: string
|
||||
@@ -66,6 +73,7 @@ const typebotContext = createContext<
|
||||
customDomain?: string | null
|
||||
}) => void
|
||||
publishTypebot: () => void
|
||||
restorePublishedTypebot: () => void
|
||||
} & BlocksActions &
|
||||
StepsActions &
|
||||
ItemsActions &
|
||||
@@ -88,27 +96,22 @@ export const TypebotContext = ({
|
||||
status: 'error',
|
||||
})
|
||||
|
||||
const { typebot, publishedTypebot, owner, isReadOnly, isLoading, mutate } =
|
||||
useFetchedTypebot({
|
||||
typebotId,
|
||||
onError: (error) =>
|
||||
toast({
|
||||
title: 'Error while fetching typebot',
|
||||
description: error.message,
|
||||
}),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!typebot ||
|
||||
!localTypebot ||
|
||||
typebot.updatedAt <= localTypebot.updatedAt ||
|
||||
deepEqual(typebot, localTypebot)
|
||||
)
|
||||
return
|
||||
setLocalTypebot({ ...typebot })
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [typebot])
|
||||
const {
|
||||
typebot,
|
||||
publishedTypebot,
|
||||
owner,
|
||||
webhooks,
|
||||
isReadOnly,
|
||||
isLoading,
|
||||
mutate,
|
||||
} = useFetchedTypebot({
|
||||
typebotId,
|
||||
onError: (error) =>
|
||||
toast({
|
||||
title: 'Error while fetching typebot',
|
||||
description: error.message,
|
||||
}),
|
||||
})
|
||||
|
||||
const [
|
||||
{ present: localTypebot },
|
||||
@@ -133,7 +136,7 @@ export const TypebotContext = ({
|
||||
toast({ title: error.name, description: error.message })
|
||||
return
|
||||
}
|
||||
mutate({ typebot: typebotToSave })
|
||||
mutate({ typebot: typebotToSave, webhooks: webhooks ?? [] })
|
||||
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
}
|
||||
|
||||
@@ -148,6 +151,7 @@ export const TypebotContext = ({
|
||||
mutate({
|
||||
typebot: currentTypebotRef.current as Typebot,
|
||||
publishedTypebot: newPublishedTypebot,
|
||||
webhooks: webhooks ?? [],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -254,7 +258,11 @@ export const TypebotContext = ({
|
||||
})
|
||||
setIsPublishing(false)
|
||||
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 (
|
||||
<typebotContext.Provider
|
||||
value={{
|
||||
typebot: localTypebot,
|
||||
publishedTypebot,
|
||||
owner,
|
||||
webhooks: webhooks ?? [],
|
||||
isReadOnly,
|
||||
isSavingLoading,
|
||||
save: saveTypebot,
|
||||
@@ -289,7 +320,9 @@ export const TypebotContext = ({
|
||||
isPublishing,
|
||||
isPublished,
|
||||
updateTypebot: updateLocalTypebot,
|
||||
restorePublishedTypebot,
|
||||
updateOnBothTypebots,
|
||||
updateWebhook,
|
||||
...blocksActions(localTypebot as Typebot, setLocalTypebot),
|
||||
...stepsAction(localTypebot as Typebot, setLocalTypebot),
|
||||
...variablesAction(localTypebot as Typebot, setLocalTypebot),
|
||||
@@ -314,6 +347,7 @@ export const useFetchedTypebot = ({
|
||||
const { data, error, mutate } = useSWR<
|
||||
{
|
||||
typebot: Typebot
|
||||
webhooks: Webhook[]
|
||||
publishedTypebot?: PublicTypebot
|
||||
owner?: User
|
||||
isReadOnly?: boolean
|
||||
@@ -323,6 +357,7 @@ export const useFetchedTypebot = ({
|
||||
if (error) onError(error)
|
||||
return {
|
||||
typebot: data?.typebot,
|
||||
webhooks: data?.webhooks,
|
||||
publishedTypebot: data?.publishedTypebot,
|
||||
owner: data?.owner,
|
||||
isReadOnly: data?.isReadOnly,
|
||||
|
||||
@@ -54,10 +54,10 @@ const stepsAction = (
|
||||
detachStepFromBlock: (indices: StepIndices) => {
|
||||
setTypebot(produce(typebot, removeStepFromBlock(indices)))
|
||||
},
|
||||
deleteStep: (indices: StepIndices) => {
|
||||
deleteStep: ({ blockIndex, stepIndex }: StepIndices) => {
|
||||
setTypebot(
|
||||
produce(typebot, (typebot) => {
|
||||
removeStepFromBlock(indices)(typebot)
|
||||
removeStepFromBlock({ blockIndex, stepIndex })(typebot)
|
||||
removeEmptyBlocks(typebot)
|
||||
})
|
||||
)
|
||||
@@ -91,7 +91,7 @@ const createStepDraft = (
|
||||
removeEmptyBlocks(typebot)
|
||||
}
|
||||
|
||||
const createNewStep = (
|
||||
const createNewStep = async (
|
||||
typebot: WritableDraft<Typebot>,
|
||||
type: DraggableStepType,
|
||||
blockId: string,
|
||||
|
||||
@@ -22,10 +22,17 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
publishedTypebot: true,
|
||||
owner: { select: { email: true, name: true, image: true } },
|
||||
collaborators: { select: { userId: true, type: true } },
|
||||
webhooks: true,
|
||||
},
|
||||
})
|
||||
if (!typebot) return res.send({ typebot: null })
|
||||
const { publishedTypebot, owner, collaborators, ...restOfTypebot } = typebot
|
||||
const {
|
||||
publishedTypebot,
|
||||
owner,
|
||||
collaborators,
|
||||
webhooks,
|
||||
...restOfTypebot
|
||||
} = typebot
|
||||
const isReadOnly =
|
||||
collaborators.find((c) => c.userId === user.id)?.type ===
|
||||
CollaborationType.READ
|
||||
@@ -34,6 +41,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
publishedTypebot,
|
||||
owner,
|
||||
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",
|
||||
"type": "Webhook",
|
||||
"options": { "responseVariableMapping": [], "variablesForTest": [] },
|
||||
"webhook": {
|
||||
"id": "2L9mPYsLAXdXwcnGVK6pv9",
|
||||
"method": "GET",
|
||||
"headers": [],
|
||||
"queryParams": []
|
||||
}
|
||||
"webhookId": "webhook1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import {
|
||||
Block,
|
||||
CredentialsType,
|
||||
defaultSettings,
|
||||
defaultTheme,
|
||||
PublicBlock,
|
||||
PublicTypebot,
|
||||
Step,
|
||||
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 = (
|
||||
userId: string,
|
||||
typebotId: string,
|
||||
@@ -139,7 +140,7 @@ const parseTypebotToPublicTypebot = (
|
||||
): Omit<PublicTypebot, 'createdAt' | 'updatedAt'> => ({
|
||||
id,
|
||||
name: typebot.name,
|
||||
blocks: parseBlocksToPublicBlocks(typebot.blocks),
|
||||
blocks: typebot.blocks,
|
||||
typebotId: typebot.id,
|
||||
theme: typebot.theme,
|
||||
settings: typebot.settings,
|
||||
@@ -149,14 +150,6 @@ const parseTypebotToPublicTypebot = (
|
||||
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 => ({
|
||||
id: partialTypebot.id ?? 'typebot',
|
||||
folderId: null,
|
||||
@@ -232,6 +225,6 @@ export const importTypebotInDatabase = async (
|
||||
data: parseTypebotToPublicTypebot(
|
||||
updates?.id ? `${updates?.id}-public` : 'publicBot',
|
||||
typebot
|
||||
),
|
||||
) as any,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import test, { expect, Page } from '@playwright/test'
|
||||
import { importTypebotInDatabase } from '../../services/database'
|
||||
import { createWebhook, importTypebotInDatabase } from '../../services/database'
|
||||
import path from 'path'
|
||||
import { generate } from 'short-uuid'
|
||||
|
||||
@@ -13,6 +13,7 @@ test.describe('Webhook step', () => {
|
||||
id: typebotId,
|
||||
}
|
||||
)
|
||||
await createWebhook(typebotId)
|
||||
|
||||
await page.goto(`/typebots/${typebotId}/edit`)
|
||||
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 { HStack, Text } from '@chakra-ui/react'
|
||||
import { CalendarIcon, CodeIcon } from 'assets/icons'
|
||||
import { StepIcon } from 'components/editor/StepsSideBar/StepIcon'
|
||||
import { byId, isInputStep, sendRequest } from 'utils'
|
||||
import { isDefined } from '@udecode/plate-common'
|
||||
|
||||
export const parseTypebotToPublicTypebot = (
|
||||
typebot: Typebot
|
||||
): PublicTypebot => ({
|
||||
id: shortId.generate(),
|
||||
typebotId: typebot.id,
|
||||
blocks: parseBlocksToPublicBlocks(typebot.blocks),
|
||||
blocks: typebot.blocks,
|
||||
edges: typebot.edges,
|
||||
name: typebot.name,
|
||||
publicId: typebot.publicId,
|
||||
@@ -19,18 +18,29 @@ export const parseTypebotToPublicTypebot = (
|
||||
theme: typebot.theme,
|
||||
variables: typebot.variables,
|
||||
customDomain: typebot.customDomain,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
|
||||
export const parseBlocksToPublicBlocks = (blocks: Block[]): PublicBlock[] =>
|
||||
blocks.map((b) => ({
|
||||
...b,
|
||||
steps: b.steps.map(
|
||||
(s) =>
|
||||
('webhook' in s && isDefined(s.webhook)
|
||||
? { ...s, webhook: s.webhook.id }
|
||||
: s) as PublicStep
|
||||
),
|
||||
}))
|
||||
export const parsePublicTypebotToTypebot = (
|
||||
typebot: PublicTypebot,
|
||||
existingTypebot: Typebot
|
||||
): Typebot => ({
|
||||
id: typebot.typebotId,
|
||||
blocks: typebot.blocks,
|
||||
edges: typebot.edges,
|
||||
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) =>
|
||||
sendRequest<PublicTypebot>({
|
||||
|
||||
@@ -29,8 +29,6 @@ import {
|
||||
defaultGoogleAnalyticsOptions,
|
||||
defaultWebhookOptions,
|
||||
StepWithOptionsType,
|
||||
defaultWebhookAttributes,
|
||||
Webhook,
|
||||
Item,
|
||||
ItemType,
|
||||
defaultConditionContent,
|
||||
@@ -39,7 +37,7 @@ import {
|
||||
import shortId, { generate } from 'short-uuid'
|
||||
import { Typebot } from 'models'
|
||||
import useSWR from 'swr'
|
||||
import { fetcher, omit, toKebabCase } from '../utils'
|
||||
import { fetcher, toKebabCase } from '../utils'
|
||||
import {
|
||||
isBubbleStepType,
|
||||
stepTypeHasItems,
|
||||
@@ -48,8 +46,8 @@ import {
|
||||
} from 'utils'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { stringify } from 'qs'
|
||||
import { isChoiceInput, isConditionStep, sendRequest } from 'utils'
|
||||
import { parseBlocksToPublicBlocks } from '../publicTypebot'
|
||||
import { isChoiceInput, isConditionStep, sendRequest, omit } from 'utils'
|
||||
import cuid from 'cuid'
|
||||
|
||||
export type TypebotInDashboard = Pick<
|
||||
Typebot,
|
||||
@@ -173,16 +171,11 @@ export const parseNewStep = (
|
||||
options: stepTypeHasOption(type)
|
||||
? parseDefaultStepOptions(type)
|
||||
: undefined,
|
||||
webhook: stepTypeHasWebhook(type) ? parseDefaultWebhook() : undefined,
|
||||
webhookId: stepTypeHasWebhook(type) ? cuid() : undefined,
|
||||
items: stepTypeHasItems(type) ? parseDefaultItems(type, id) : undefined,
|
||||
} as DraggableStep
|
||||
}
|
||||
|
||||
const parseDefaultWebhook = (): Webhook => ({
|
||||
id: generate(),
|
||||
...defaultWebhookAttributes,
|
||||
})
|
||||
|
||||
const parseDefaultItems = (
|
||||
type: LogicStepType.CONDITION | InputStepType.CHOICE,
|
||||
stepId: string
|
||||
@@ -255,7 +248,7 @@ export const checkIfPublished = (
|
||||
typebot: Typebot,
|
||||
publicTypebot: PublicTypebot
|
||||
) =>
|
||||
deepEqual(parseBlocksToPublicBlocks(typebot.blocks), publicTypebot.blocks) &&
|
||||
deepEqual(typebot.blocks, publicTypebot.blocks) &&
|
||||
deepEqual(typebot.settings, publicTypebot.settings) &&
|
||||
deepEqual(typebot.theme, publicTypebot.theme) &&
|
||||
deepEqual(typebot.variables, publicTypebot.variables)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import imageCompression from 'browser-image-compression'
|
||||
import { Parser } from 'htmlparser2'
|
||||
import { PublicStep, Step, Typebot } from 'models'
|
||||
import { Step, Typebot } from 'models'
|
||||
|
||||
export const fetcher = async (input: RequestInfo, init?: RequestInit) => {
|
||||
const res = await fetch(input, init)
|
||||
@@ -36,26 +36,6 @@ export const toKebabCase = (value: string) => {
|
||||
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) => {
|
||||
const res = await fetch(
|
||||
`/api/storage/upload-url?key=${encodeURIComponent(
|
||||
@@ -98,7 +78,7 @@ export const removeUndefinedFields = <T>(obj: T): 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) => {
|
||||
const varNames = typebot.variables.map((v) => v.name)
|
||||
@@ -128,3 +108,32 @@ export const readFile = (file: File): Promise<string> => {
|
||||
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,
|
||||
})
|
||||
Reference in New Issue
Block a user