@@ -1,7 +1,11 @@
|
||||
import { TextInput } from '@/components/inputs'
|
||||
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
|
||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||
import { EmailInputOptions, Variable } from '@typebot.io/schemas'
|
||||
import {
|
||||
EmailInputOptions,
|
||||
Variable,
|
||||
invalidEmailDefaultRetryMessage,
|
||||
} from '@typebot.io/schemas'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
@@ -33,7 +37,9 @@ export const EmailInputSettings = ({ options, onOptionsChange }: Props) => {
|
||||
/>
|
||||
<TextInput
|
||||
label="Retry message:"
|
||||
defaultValue={options.retryMessageContent}
|
||||
defaultValue={
|
||||
options.retryMessageContent ?? invalidEmailDefaultRetryMessage
|
||||
}
|
||||
onChange={handleRetryMessageChange}
|
||||
/>
|
||||
<Stack>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import test, { expect } from '@playwright/test'
|
||||
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
|
||||
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
|
||||
import { defaultEmailInputOptions, InputBlockType } from '@typebot.io/schemas'
|
||||
import {
|
||||
defaultEmailInputOptions,
|
||||
InputBlockType,
|
||||
invalidEmailDefaultRetryMessage,
|
||||
} from '@typebot.io/schemas'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
|
||||
test.describe('Email input block', () => {
|
||||
@@ -35,7 +39,7 @@ test.describe('Email input block', () => {
|
||||
await expect(page.locator('text=Your email...')).toBeVisible()
|
||||
await page.getByLabel('Button label:').fill('Go')
|
||||
await page.fill(
|
||||
`input[value="${defaultEmailInputOptions.retryMessageContent}"]`,
|
||||
`input[value="${invalidEmailDefaultRetryMessage}"]`,
|
||||
'Try again bro'
|
||||
)
|
||||
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { MakeComBlock } from '@typebot.io/schemas'
|
||||
import { byId, isNotDefined } from '@typebot.io/lib'
|
||||
import { isNotDefined } from '@typebot.io/lib'
|
||||
|
||||
type Props = {
|
||||
block: MakeComBlock
|
||||
}
|
||||
|
||||
export const MakeComContent = ({ block }: Props) => {
|
||||
const { webhooks } = useTypebot()
|
||||
const webhook = block.options.webhook ?? webhooks.find(byId(block.webhookId))
|
||||
const webhook = block.options.webhook
|
||||
|
||||
if (isNotDefined(webhook?.body))
|
||||
return <Text color="gray.500">Configure...</Text>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Alert, AlertIcon, Button, Link, Stack, Text } from '@chakra-ui/react'
|
||||
import { ExternalLinkIcon } from '@/components/icons'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { MakeComBlock, Webhook, WebhookOptions } from '@typebot.io/schemas'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { byId } from '@typebot.io/lib'
|
||||
import React from 'react'
|
||||
import { WebhookAdvancedConfigForm } from '../../webhook/components/WebhookAdvancedConfigForm'
|
||||
|
||||
type Props = {
|
||||
@@ -12,45 +10,18 @@ type Props = {
|
||||
}
|
||||
|
||||
export const MakeComSettings = ({
|
||||
block: { webhookId, id: blockId, options },
|
||||
block: { id: blockId, options },
|
||||
onOptionsChange,
|
||||
}: Props) => {
|
||||
const { webhooks, updateWebhook } = useTypebot()
|
||||
const webhook = webhooks.find(byId(webhookId))
|
||||
|
||||
const [localWebhook, _setLocalWebhook] = useState(webhook)
|
||||
|
||||
const setLocalWebhook = useCallback(
|
||||
async (newLocalWebhook: Webhook) => {
|
||||
if (options.webhook) {
|
||||
onOptionsChange({
|
||||
...options,
|
||||
webhook: newLocalWebhook,
|
||||
})
|
||||
return
|
||||
}
|
||||
_setLocalWebhook(newLocalWebhook)
|
||||
await updateWebhook(newLocalWebhook.id, newLocalWebhook)
|
||||
},
|
||||
[onOptionsChange, options, updateWebhook]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!localWebhook ||
|
||||
localWebhook.url ||
|
||||
!webhook?.url ||
|
||||
webhook.url === localWebhook.url ||
|
||||
options.webhook
|
||||
)
|
||||
return
|
||||
setLocalWebhook({
|
||||
...localWebhook,
|
||||
url: webhook?.url,
|
||||
const setLocalWebhook = async (newLocalWebhook: Webhook) => {
|
||||
if (!options.webhook) return
|
||||
onOptionsChange({
|
||||
...options,
|
||||
webhook: newLocalWebhook,
|
||||
})
|
||||
}, [webhook, localWebhook, setLocalWebhook, options.webhook])
|
||||
}
|
||||
|
||||
const url = options.webhook?.url ?? localWebhook?.url
|
||||
const url = options.webhook?.url
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
@@ -72,10 +43,10 @@ export const MakeComSettings = ({
|
||||
</Stack>
|
||||
)}
|
||||
</Alert>
|
||||
{(localWebhook || options.webhook) && (
|
||||
{options.webhook && (
|
||||
<WebhookAdvancedConfigForm
|
||||
blockId={blockId}
|
||||
webhook={(options.webhook ?? localWebhook) as Webhook}
|
||||
webhook={options.webhook as Webhook}
|
||||
options={options}
|
||||
onWebhookChange={setLocalWebhook}
|
||||
onOptionsChange={onOptionsChange}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { PabblyConnectBlock } from '@typebot.io/schemas'
|
||||
import { byId, isNotDefined } from '@typebot.io/lib'
|
||||
import { isNotDefined } from '@typebot.io/lib'
|
||||
|
||||
type Props = {
|
||||
block: PabblyConnectBlock
|
||||
}
|
||||
|
||||
export const PabblyConnectContent = ({ block }: Props) => {
|
||||
const { webhooks } = useTypebot()
|
||||
const webhook = block.options.webhook ?? webhooks.find(byId(block.webhookId))
|
||||
const webhook = block.options.webhook
|
||||
|
||||
if (isNotDefined(webhook?.body))
|
||||
return <Text color="gray.500">Configure...</Text>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Alert, AlertIcon, Button, Link, Stack, Text } from '@chakra-ui/react'
|
||||
import { ExternalLinkIcon } from '@/components/icons'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import {
|
||||
PabblyConnectBlock,
|
||||
Webhook,
|
||||
WebhookOptions,
|
||||
} from '@typebot.io/schemas'
|
||||
import React, { useState } from 'react'
|
||||
import { byId } from '@typebot.io/lib'
|
||||
import React from 'react'
|
||||
import { WebhookAdvancedConfigForm } from '../../webhook/components/WebhookAdvancedConfigForm'
|
||||
import { TextInput } from '@/components/inputs'
|
||||
|
||||
@@ -17,35 +15,23 @@ type Props = {
|
||||
}
|
||||
|
||||
export const PabblyConnectSettings = ({
|
||||
block: { webhookId, id: blockId, options },
|
||||
block: { id: blockId, options },
|
||||
onOptionsChange,
|
||||
}: Props) => {
|
||||
const { webhooks, updateWebhook } = useTypebot()
|
||||
|
||||
const [localWebhook, _setLocalWebhook] = useState(
|
||||
webhooks.find(byId(webhookId))
|
||||
)
|
||||
|
||||
const setLocalWebhook = async (newLocalWebhook: Webhook) => {
|
||||
if (options.webhook) {
|
||||
onOptionsChange({
|
||||
...options,
|
||||
webhook: newLocalWebhook,
|
||||
})
|
||||
return
|
||||
}
|
||||
_setLocalWebhook(newLocalWebhook)
|
||||
await updateWebhook(newLocalWebhook.id, newLocalWebhook)
|
||||
if (!options.webhook) return
|
||||
onOptionsChange({
|
||||
...options,
|
||||
webhook: newLocalWebhook,
|
||||
})
|
||||
}
|
||||
|
||||
const handleUrlChange = (url: string) =>
|
||||
localWebhook &&
|
||||
setLocalWebhook({
|
||||
...localWebhook,
|
||||
url,
|
||||
})
|
||||
const updateUrl = (url: string) => {
|
||||
if (!options.webhook) return
|
||||
onOptionsChange({ ...options, webhook: { ...options.webhook, url } })
|
||||
}
|
||||
|
||||
const url = options.webhook?.url ?? localWebhook?.url
|
||||
const url = options.webhook?.url
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
@@ -70,14 +56,14 @@ export const PabblyConnectSettings = ({
|
||||
<TextInput
|
||||
placeholder="Paste webhook URL..."
|
||||
defaultValue={url ?? ''}
|
||||
onChange={handleUrlChange}
|
||||
onChange={updateUrl}
|
||||
withVariableButton={false}
|
||||
debounceTimeout={0}
|
||||
/>
|
||||
{(localWebhook || options.webhook) && (
|
||||
{options.webhook && (
|
||||
<WebhookAdvancedConfigForm
|
||||
blockId={blockId}
|
||||
webhook={(options.webhook ?? localWebhook) as Webhook}
|
||||
webhook={options.webhook as Webhook}
|
||||
options={options}
|
||||
onWebhookChange={setLocalWebhook}
|
||||
onOptionsChange={onOptionsChange}
|
||||
|
||||
@@ -47,7 +47,7 @@ export const WebhookAdvancedConfigForm = ({
|
||||
onWebhookChange,
|
||||
onOptionsChange,
|
||||
}: Props) => {
|
||||
const { typebot, save, updateWebhook } = useTypebot()
|
||||
const { typebot, save } = useTypebot()
|
||||
const [isTestResponseLoading, setIsTestResponseLoading] = useState(false)
|
||||
const [testResponse, setTestResponse] = useState<string>()
|
||||
const [responseKeys, setResponseKeys] = useState<string[]>([])
|
||||
@@ -80,8 +80,7 @@ export const WebhookAdvancedConfigForm = ({
|
||||
const executeTestRequest = async () => {
|
||||
if (!typebot) return
|
||||
setIsTestResponseLoading(true)
|
||||
if (!options.webhook)
|
||||
await Promise.all([updateWebhook(webhook.id, webhook), save()])
|
||||
if (!options.webhook) await save()
|
||||
else await save()
|
||||
const { data, error } = await executeWebhook(
|
||||
typebot.id,
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Stack, Text } from '@chakra-ui/react'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { WebhookBlock } from '@typebot.io/schemas'
|
||||
import { byId } from '@typebot.io/lib'
|
||||
import { SetVariableLabel } from '@/components/SetVariableLabel'
|
||||
|
||||
type Props = {
|
||||
block: WebhookBlock
|
||||
}
|
||||
|
||||
export const WebhookContent = ({ block: { options, webhookId } }: Props) => {
|
||||
export const WebhookContent = ({ block: { options } }: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
const { webhooks } = useTypebot()
|
||||
const webhook = options.webhook ?? webhooks.find(byId(webhookId))
|
||||
const webhook = options.webhook
|
||||
|
||||
if (!webhook?.url) return <Text color="gray.500">Configure...</Text>
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React, { useState } from 'react'
|
||||
import React from 'react'
|
||||
import { Spinner, Stack } from '@chakra-ui/react'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { WebhookOptions, Webhook, WebhookBlock } from '@typebot.io/schemas'
|
||||
import { byId } from '@typebot.io/lib'
|
||||
import { TextInput } from '@/components/inputs'
|
||||
import { WebhookAdvancedConfigForm } from './WebhookAdvancedConfigForm'
|
||||
|
||||
@@ -12,42 +10,32 @@ type Props = {
|
||||
}
|
||||
|
||||
export const WebhookSettings = ({
|
||||
block: { webhookId, id: blockId, options },
|
||||
block: { id: blockId, options },
|
||||
onOptionsChange,
|
||||
}: Props) => {
|
||||
const { webhooks, updateWebhook } = useTypebot()
|
||||
const [localWebhook, _setLocalWebhook] = useState(
|
||||
webhooks.find(byId(webhookId))
|
||||
)
|
||||
|
||||
const setLocalWebhook = async (newLocalWebhook: Webhook) => {
|
||||
if (options.webhook) {
|
||||
onOptionsChange({ ...options, webhook: newLocalWebhook })
|
||||
return
|
||||
}
|
||||
_setLocalWebhook(newLocalWebhook)
|
||||
await updateWebhook(newLocalWebhook.id, newLocalWebhook)
|
||||
if (!options.webhook) return
|
||||
onOptionsChange({ ...options, webhook: newLocalWebhook })
|
||||
return
|
||||
}
|
||||
|
||||
const updateUrl = (url: string) => {
|
||||
if (options.webhook)
|
||||
onOptionsChange({ ...options, webhook: { ...options.webhook, url } })
|
||||
else if (localWebhook)
|
||||
setLocalWebhook({ ...localWebhook, url: url ?? undefined })
|
||||
if (!options.webhook) return
|
||||
onOptionsChange({ ...options, webhook: { ...options.webhook, url } })
|
||||
}
|
||||
|
||||
if (!localWebhook && !options.webhook) return <Spinner />
|
||||
if (!options.webhook) return <Spinner />
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<TextInput
|
||||
placeholder="Paste webhook URL..."
|
||||
defaultValue={options.webhook?.url ?? localWebhook?.url ?? ''}
|
||||
defaultValue={options.webhook?.url ?? ''}
|
||||
onChange={updateUrl}
|
||||
/>
|
||||
<WebhookAdvancedConfigForm
|
||||
blockId={blockId}
|
||||
webhook={(options.webhook ?? localWebhook) as Webhook}
|
||||
webhook={options.webhook as Webhook}
|
||||
options={options}
|
||||
onWebhookChange={setLocalWebhook}
|
||||
onOptionsChange={onOptionsChange}
|
||||
|
||||
@@ -174,7 +174,7 @@ test.describe('API', () => {
|
||||
expect(data.resultExample).toMatchObject({
|
||||
message: 'This is a sample result, it has been generated ⬇️',
|
||||
Welcome: 'Hi!',
|
||||
Email: 'test@email.com',
|
||||
Email: 'user@email.com',
|
||||
Name: 'answer value',
|
||||
Services: 'Website dev, Content Marketing, Social Media, UI / UX Design',
|
||||
'Additional information': 'answer value',
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { ZapierBlock } from '@typebot.io/schemas'
|
||||
import { byId, isNotDefined } from '@typebot.io/lib'
|
||||
import { isNotDefined } from '@typebot.io/lib'
|
||||
|
||||
type Props = {
|
||||
block: ZapierBlock
|
||||
}
|
||||
|
||||
export const ZapierContent = ({ block }: Props) => {
|
||||
const { webhooks } = useTypebot()
|
||||
const webhook = block.options.webhook ?? webhooks.find(byId(block.webhookId))
|
||||
const webhook = block.options.webhook
|
||||
|
||||
if (isNotDefined(webhook?.body))
|
||||
return <Text color="gray.500">Configure...</Text>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Alert, AlertIcon, Button, Link, Stack, Text } from '@chakra-ui/react'
|
||||
import { ExternalLinkIcon } from '@/components/icons'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { Webhook, WebhookOptions, ZapierBlock } from '@typebot.io/schemas'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { byId } from '@typebot.io/lib'
|
||||
import React from 'react'
|
||||
import { WebhookAdvancedConfigForm } from '../../webhook/components/WebhookAdvancedConfigForm'
|
||||
|
||||
type Props = {
|
||||
@@ -12,45 +10,19 @@ type Props = {
|
||||
}
|
||||
|
||||
export const ZapierSettings = ({
|
||||
block: { webhookId, id: blockId, options },
|
||||
block: { id: blockId, options },
|
||||
onOptionsChange,
|
||||
}: Props) => {
|
||||
const { webhooks, updateWebhook } = useTypebot()
|
||||
const webhook = webhooks.find(byId(webhookId))
|
||||
|
||||
const [localWebhook, _setLocalWebhook] = useState(webhook)
|
||||
|
||||
const setLocalWebhook = useCallback(
|
||||
async (newLocalWebhook: Webhook) => {
|
||||
if (options.webhook) {
|
||||
onOptionsChange({
|
||||
...options,
|
||||
webhook: newLocalWebhook,
|
||||
})
|
||||
return
|
||||
}
|
||||
_setLocalWebhook(newLocalWebhook)
|
||||
await updateWebhook(newLocalWebhook.id, newLocalWebhook)
|
||||
},
|
||||
[onOptionsChange, options, updateWebhook]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!localWebhook ||
|
||||
localWebhook.url ||
|
||||
!webhook?.url ||
|
||||
webhook.url === localWebhook.url ||
|
||||
options.webhook
|
||||
)
|
||||
return
|
||||
setLocalWebhook({
|
||||
...localWebhook,
|
||||
url: webhook?.url,
|
||||
const setLocalWebhook = async (newLocalWebhook: Webhook) => {
|
||||
if (!options.webhook) return
|
||||
onOptionsChange({
|
||||
...options,
|
||||
webhook: newLocalWebhook,
|
||||
})
|
||||
}, [webhook, localWebhook, setLocalWebhook, options.webhook])
|
||||
return
|
||||
}
|
||||
|
||||
const url = options.webhook?.url ?? localWebhook?.url
|
||||
const url = options.webhook?.url
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
@@ -72,10 +44,10 @@ export const ZapierSettings = ({
|
||||
</Stack>
|
||||
)}
|
||||
</Alert>
|
||||
{(localWebhook || options.webhook) && (
|
||||
{options.webhook && (
|
||||
<WebhookAdvancedConfigForm
|
||||
blockId={blockId}
|
||||
webhook={(options.webhook ?? localWebhook) as Webhook}
|
||||
webhook={options.webhook as Webhook}
|
||||
options={options}
|
||||
onWebhookChange={setLocalWebhook}
|
||||
onOptionsChange={onOptionsChange}
|
||||
|
||||
@@ -33,7 +33,7 @@ const Expression = ({
|
||||
case 'Custom':
|
||||
case undefined:
|
||||
return (
|
||||
<Text>
|
||||
<Text as="span">
|
||||
{variableName} = {options.expressionToEvaluate}
|
||||
</Text>
|
||||
)
|
||||
@@ -48,7 +48,7 @@ const Expression = ({
|
||||
byId(options.mapListItemParams?.targetListVariableId)
|
||||
)
|
||||
return (
|
||||
<Text>
|
||||
<Text as="span">
|
||||
{variableName} = item in ${targetListVariable?.name} with same index
|
||||
as ${baseItemVariable?.name} in ${baseListVariable?.name}
|
||||
</Text>
|
||||
@@ -64,7 +64,7 @@ const Expression = ({
|
||||
case 'Moment of the day':
|
||||
case 'Yesterday': {
|
||||
return (
|
||||
<Text>
|
||||
<Text as="span">
|
||||
{variableName} = <Tag colorScheme="purple">System.{options.type}</Tag>
|
||||
</Text>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { z } from 'zod'
|
||||
import { collaboratorSchema } from '@typebot.io/schemas/features/collaborators'
|
||||
import { isReadTypebotForbidden } from '@/features/typebot/helpers/isReadTypebotForbidden'
|
||||
|
||||
export const getCollaborators = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/typebots/{typebotId}/collaborators',
|
||||
protect: true,
|
||||
summary: 'Get collaborators',
|
||||
tags: ['Collaborators'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
typebotId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
collaborators: z.array(collaboratorSchema),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { typebotId }, ctx: { user } }) => {
|
||||
const existingTypebot = await prisma.typebot.findFirst({
|
||||
where: {
|
||||
id: typebotId,
|
||||
},
|
||||
include: {
|
||||
collaborators: true,
|
||||
},
|
||||
})
|
||||
if (
|
||||
!existingTypebot?.id ||
|
||||
(await isReadTypebotForbidden(existingTypebot, user))
|
||||
)
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
|
||||
|
||||
return {
|
||||
collaborators: existingTypebot.collaborators,
|
||||
}
|
||||
})
|
||||
6
apps/builder/src/features/collaboration/api/router.ts
Normal file
6
apps/builder/src/features/collaboration/api/router.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { router } from '@/helpers/server/trpc'
|
||||
import { getCollaborators } from './getCollaborators'
|
||||
|
||||
export const collaboratorsRouter = router({
|
||||
getCollaborators: getCollaborators,
|
||||
})
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Typebot,
|
||||
} from '@typebot.io/schemas'
|
||||
|
||||
// TODO: remove
|
||||
export type NewTypebotProps = Omit<
|
||||
Typebot,
|
||||
| 'createdAt'
|
||||
@@ -56,12 +57,6 @@ export const parseNewTypebot = ({
|
||||
variables: [],
|
||||
selectedThemeTemplateId: null,
|
||||
theme: defaultTheme,
|
||||
settings: {
|
||||
...defaultSettings,
|
||||
general: {
|
||||
...defaultSettings.general,
|
||||
isBrandingEnabled,
|
||||
},
|
||||
},
|
||||
settings: defaultSettings({ isBrandingEnabled }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Typebot } from '@typebot.io/schemas'
|
||||
import { sendRequest } from '@typebot.io/lib'
|
||||
|
||||
export const createTypebotQuery = async ({
|
||||
folderId,
|
||||
workspaceId,
|
||||
}: Pick<Typebot, 'folderId' | 'workspaceId'>) => {
|
||||
const typebot = {
|
||||
folderId,
|
||||
name: 'My typebot',
|
||||
workspaceId,
|
||||
}
|
||||
return sendRequest<Typebot>({
|
||||
url: `/api/typebots`,
|
||||
method: 'POST',
|
||||
body: typebot,
|
||||
})
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { sendRequest } from '@typebot.io/lib'
|
||||
|
||||
export const deleteTypebotQuery = async (id: string) =>
|
||||
sendRequest({
|
||||
url: `/api/typebots/${id}`,
|
||||
method: 'DELETE',
|
||||
})
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Typebot } from '@typebot.io/schemas'
|
||||
import { sendRequest } from '@typebot.io/lib'
|
||||
|
||||
export const getTypebotQuery = (typebotId: string) =>
|
||||
sendRequest<{ typebot: Typebot }>({
|
||||
url: `/api/typebots/${typebotId}`,
|
||||
method: 'GET',
|
||||
})
|
||||
@@ -1,174 +0,0 @@
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { Plan, Prisma } from '@typebot.io/prisma'
|
||||
import {
|
||||
ChoiceInputBlock,
|
||||
ConditionBlock,
|
||||
LogicBlockType,
|
||||
Typebot,
|
||||
} from '@typebot.io/schemas'
|
||||
import { JumpBlock } from '@typebot.io/schemas/features/blocks/logic/jump'
|
||||
import {
|
||||
blockHasItems,
|
||||
isDefined,
|
||||
isWebhookBlock,
|
||||
sendRequest,
|
||||
} from '@typebot.io/lib'
|
||||
import { duplicateWebhookQuery } from '@/features/blocks/integrations/webhook/queries/duplicateWebhookQuery'
|
||||
|
||||
export const importTypebotQuery = async (typebot: Typebot, userPlan: Plan) => {
|
||||
const { typebot: newTypebot, webhookIdsMapping } = duplicateTypebot(
|
||||
typebot,
|
||||
userPlan
|
||||
)
|
||||
const { data, error } = await sendRequest<Typebot>({
|
||||
url: `/api/typebots`,
|
||||
method: 'POST',
|
||||
body: newTypebot,
|
||||
})
|
||||
if (!data) return { data, error }
|
||||
const webhookBlocks = typebot.groups
|
||||
.flatMap((b) => b.blocks)
|
||||
.filter(isWebhookBlock)
|
||||
.filter((block) => block.webhookId)
|
||||
await Promise.all(
|
||||
webhookBlocks.map((webhookBlock) =>
|
||||
duplicateWebhookQuery({
|
||||
existingIds: {
|
||||
typebotId: typebot.id,
|
||||
webhookId: webhookBlock.webhookId as string,
|
||||
},
|
||||
newIds: {
|
||||
typebotId: newTypebot.id,
|
||||
webhookId: webhookIdsMapping.get(
|
||||
webhookBlock.webhookId as string
|
||||
) as string,
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
return { data, error }
|
||||
}
|
||||
|
||||
const duplicateTypebot = (
|
||||
typebot: Typebot,
|
||||
userPlan: Plan
|
||||
): {
|
||||
typebot: Omit<Prisma.TypebotUncheckedCreateInput, 'id'> & { id: string }
|
||||
webhookIdsMapping: Map<string, string>
|
||||
} => {
|
||||
const groupIdsMapping = generateOldNewIdsMapping(typebot.groups)
|
||||
const blockIdsMapping = generateOldNewIdsMapping(
|
||||
typebot.groups.flatMap((group) => group.blocks)
|
||||
)
|
||||
const edgeIdsMapping = generateOldNewIdsMapping(typebot.edges)
|
||||
const webhookIdsMapping = generateOldNewIdsMapping(
|
||||
typebot.groups
|
||||
.flatMap((group) => group.blocks)
|
||||
.filter(isWebhookBlock)
|
||||
.map((block) => ({
|
||||
id: block.webhookId as string,
|
||||
}))
|
||||
)
|
||||
const id = createId()
|
||||
return {
|
||||
typebot: {
|
||||
...typebot,
|
||||
id,
|
||||
name: `${typebot.name} copy`,
|
||||
publicId: null,
|
||||
customDomain: null,
|
||||
groups: typebot.groups.map((group) => ({
|
||||
...group,
|
||||
id: groupIdsMapping.get(group.id) as string,
|
||||
blocks: group.blocks.map((block) => {
|
||||
const newIds = {
|
||||
id: blockIdsMapping.get(block.id) as string,
|
||||
groupId: groupIdsMapping.get(block.groupId) as string,
|
||||
outgoingEdgeId: block.outgoingEdgeId
|
||||
? edgeIdsMapping.get(block.outgoingEdgeId)
|
||||
: undefined,
|
||||
}
|
||||
if (
|
||||
block.type === LogicBlockType.TYPEBOT_LINK &&
|
||||
block.options.typebotId === 'current' &&
|
||||
isDefined(block.options.groupId)
|
||||
)
|
||||
return {
|
||||
...block,
|
||||
...newIds,
|
||||
options: {
|
||||
...block.options,
|
||||
groupId: groupIdsMapping.get(block.options.groupId as string),
|
||||
},
|
||||
}
|
||||
if (block.type === LogicBlockType.JUMP)
|
||||
return {
|
||||
...block,
|
||||
...newIds,
|
||||
options: {
|
||||
...block.options,
|
||||
groupId: groupIdsMapping.get(block.options.groupId as string),
|
||||
blockId: blockIdsMapping.get(block.options.blockId as string),
|
||||
} satisfies JumpBlock['options'],
|
||||
}
|
||||
if (blockHasItems(block))
|
||||
return {
|
||||
...block,
|
||||
...newIds,
|
||||
items: block.items.map((item) => ({
|
||||
...item,
|
||||
outgoingEdgeId: item.outgoingEdgeId
|
||||
? (edgeIdsMapping.get(item.outgoingEdgeId) as string)
|
||||
: undefined,
|
||||
})),
|
||||
} as ChoiceInputBlock | ConditionBlock
|
||||
if (isWebhookBlock(block) && block.webhookId) {
|
||||
return {
|
||||
...block,
|
||||
...newIds,
|
||||
webhookId: webhookIdsMapping.get(block.webhookId) as string,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...block,
|
||||
...newIds,
|
||||
}
|
||||
}),
|
||||
})),
|
||||
edges: typebot.edges.map((edge) => ({
|
||||
...edge,
|
||||
id: edgeIdsMapping.get(edge.id) as string,
|
||||
from: {
|
||||
...edge.from,
|
||||
blockId: blockIdsMapping.get(edge.from.blockId) as string,
|
||||
groupId: groupIdsMapping.get(edge.from.groupId) as string,
|
||||
},
|
||||
to: {
|
||||
...edge.to,
|
||||
blockId: edge.to.blockId
|
||||
? (blockIdsMapping.get(edge.to.blockId) as string)
|
||||
: undefined,
|
||||
groupId: groupIdsMapping.get(edge.to.groupId) as string,
|
||||
},
|
||||
})),
|
||||
settings:
|
||||
typebot.settings.general.isBrandingEnabled === false &&
|
||||
userPlan === Plan.FREE
|
||||
? {
|
||||
...typebot.settings,
|
||||
general: { ...typebot.settings.general, isBrandingEnabled: true },
|
||||
}
|
||||
: typebot.settings,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
resultsTablePreferences: typebot.resultsTablePreferences ?? undefined,
|
||||
},
|
||||
webhookIdsMapping,
|
||||
}
|
||||
}
|
||||
|
||||
const generateOldNewIdsMapping = (itemWithId: { id: string }[]) => {
|
||||
const idsMapping: Map<string, string> = new Map()
|
||||
itemWithId.forEach((item) => idsMapping.set(item.id, createId()))
|
||||
return idsMapping
|
||||
}
|
||||
@@ -52,9 +52,11 @@ export const TypebotHeader = () => {
|
||||
}, 1000)
|
||||
const { isOpen, onOpen } = useDisclosure()
|
||||
|
||||
const handleNameSubmit = (name: string) => updateTypebot({ name })
|
||||
const handleNameSubmit = (name: string) =>
|
||||
updateTypebot({ updates: { name } })
|
||||
|
||||
const handleChangeIcon = (icon: string) => updateTypebot({ icon })
|
||||
const handleChangeIcon = (icon: string) =>
|
||||
updateTypebot({ updates: { icon } })
|
||||
|
||||
const handlePreviewClick = async () => {
|
||||
setStartPreviewAtGroup(undefined)
|
||||
|
||||
@@ -5,11 +5,6 @@ import {
|
||||
createTypebots,
|
||||
importTypebotInDatabase,
|
||||
} from '@typebot.io/lib/playwright/databaseActions'
|
||||
import {
|
||||
waitForSuccessfulDeleteRequest,
|
||||
waitForSuccessfulPostRequest,
|
||||
waitForSuccessfulPutRequest,
|
||||
} from '@typebot.io/lib/playwright/testHelpers'
|
||||
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
|
||||
@@ -236,25 +231,13 @@ test('Published typebot menu should work', async ({ page }) => {
|
||||
await expect(page.locator("text='Start'")).toBeVisible()
|
||||
await expect(page.locator('button >> text="Published"')).toBeVisible()
|
||||
await page.click('[aria-label="Show published typebot menu"]')
|
||||
await Promise.all([
|
||||
waitForSuccessfulPutRequest(page),
|
||||
page.click('text="Close typebot to new responses"'),
|
||||
])
|
||||
await page.click('text="Close typebot to new responses"')
|
||||
await expect(page.locator('button >> text="Closed"')).toBeDisabled()
|
||||
await page.click('[aria-label="Show published typebot menu"]')
|
||||
await Promise.all([
|
||||
waitForSuccessfulPutRequest(page),
|
||||
page.click('text="Reopen typebot to new responses"'),
|
||||
])
|
||||
await page.click('text="Reopen typebot to new responses"')
|
||||
await expect(page.locator('button >> text="Published"')).toBeDisabled()
|
||||
await page.click('[aria-label="Show published typebot menu"]')
|
||||
await Promise.all([
|
||||
waitForSuccessfulDeleteRequest(page),
|
||||
page.click('button >> text="Unpublish typebot"'),
|
||||
])
|
||||
await Promise.all([
|
||||
waitForSuccessfulPostRequest(page),
|
||||
page.click('button >> text="Publish"'),
|
||||
])
|
||||
await page.click('button >> text="Unpublish typebot"')
|
||||
await page.click('button >> text="Publish"')
|
||||
await expect(page.locator('button >> text="Published"')).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
LogicBlockType,
|
||||
PublicTypebot,
|
||||
Typebot,
|
||||
Webhook,
|
||||
} from '@typebot.io/schemas'
|
||||
import { LogicBlockType, PublicTypebot, Typebot } from '@typebot.io/schemas'
|
||||
import { Router, useRouter } from 'next/router'
|
||||
import {
|
||||
createContext,
|
||||
@@ -12,7 +7,6 @@ import {
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { isDefined, omit } from '@typebot.io/lib'
|
||||
import { edgesAction, EdgesActions } from './typebotActions/edges'
|
||||
@@ -22,21 +16,11 @@ import { blocksAction, BlocksActions } from './typebotActions/blocks'
|
||||
import { variablesAction, VariablesActions } from './typebotActions/variables'
|
||||
import { dequal } from 'dequal'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { useTypebotQuery } from '@/hooks/useTypebotQuery'
|
||||
import { useUndo } from '../hooks/useUndo'
|
||||
import { updateTypebotQuery } from '../queries/updateTypebotQuery'
|
||||
import { updateWebhookQuery } from '@/features/blocks/integrations/webhook/queries/updateWebhookQuery'
|
||||
import { useAutoSave } from '@/hooks/useAutoSave'
|
||||
import { createWebhookQuery } from '@/features/blocks/integrations/webhook/queries/createWebhookQuery'
|
||||
import { duplicateWebhookQuery } from '@/features/blocks/integrations/webhook/queries/duplicateWebhookQuery'
|
||||
import { parseDefaultPublicId } from '@/features/publish/helpers/parseDefaultPublicId'
|
||||
import { createPublishedTypebotQuery } from '@/features/publish/queries/createPublishedTypebotQuery'
|
||||
import { deletePublishedTypebotQuery } from '@/features/publish/queries/deletePublishedTypebotQuery'
|
||||
import { updatePublishedTypebotQuery } from '@/features/publish/queries/updatePublishedTypebotQuery'
|
||||
import { preventUserFromRefreshing } from '@/helpers/preventUserFromRefreshing'
|
||||
import { areTypebotsEqual } from '@/features/publish/helpers/areTypebotsEqual'
|
||||
import { isPublished as isPublishedHelper } from '@/features/publish/helpers/isPublished'
|
||||
import { convertTypebotToPublicTypebot } from '@/features/publish/helpers/convertTypebotToPublicTypebot'
|
||||
import { convertPublicTypebotToTypebot } from '@/features/publish/helpers/convertPublicTypebotToTypebot'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
|
||||
@@ -66,23 +50,18 @@ const typebotContext = createContext<
|
||||
typebot?: Typebot
|
||||
publishedTypebot?: PublicTypebot
|
||||
linkedTypebots?: Pick<Typebot, 'id' | 'groups' | 'variables' | 'name'>[]
|
||||
webhooks: Webhook[]
|
||||
isReadOnly?: boolean
|
||||
isPublished: boolean
|
||||
isPublishing: boolean
|
||||
isSavingLoading: boolean
|
||||
save: () => Promise<void>
|
||||
save: () => Promise<Typebot | undefined>
|
||||
undo: () => void
|
||||
redo: () => void
|
||||
canRedo: boolean
|
||||
canUndo: boolean
|
||||
updateWebhook: (
|
||||
webhookId: string,
|
||||
webhook: Partial<Webhook>
|
||||
) => Promise<void>
|
||||
updateTypebot: (updates: UpdateTypebotPayload) => void
|
||||
publishTypebot: () => void
|
||||
unpublishTypebot: () => void
|
||||
updateTypebot: (props: {
|
||||
updates: UpdateTypebotPayload
|
||||
save?: boolean
|
||||
}) => Promise<Typebot | undefined>
|
||||
restorePublishedTypebot: () => void
|
||||
} & GroupsActions &
|
||||
BlocksActions &
|
||||
@@ -104,15 +83,48 @@ export const TypebotProvider = ({
|
||||
const { showToast } = useToast()
|
||||
|
||||
const {
|
||||
typebot,
|
||||
publishedTypebot,
|
||||
webhooks,
|
||||
isReadOnly,
|
||||
data: typebotData,
|
||||
isLoading: isFetchingTypebot,
|
||||
mutate,
|
||||
} = useTypebotQuery({
|
||||
typebotId,
|
||||
})
|
||||
refetch: refetchTypebot,
|
||||
} = trpc.typebot.getTypebot.useQuery(
|
||||
{ typebotId: typebotId as string },
|
||||
{
|
||||
enabled: isDefined(typebotId),
|
||||
onError: (error) =>
|
||||
showToast({
|
||||
title: 'Error while fetching typebot. Refresh the page.',
|
||||
description: error.message,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
const { data: publishedTypebotData } =
|
||||
trpc.typebot.getPublishedTypebot.useQuery(
|
||||
{ typebotId: typebotId as string },
|
||||
{
|
||||
enabled: isDefined(typebotId),
|
||||
onError: (error) =>
|
||||
showToast({
|
||||
title: 'Error while fetching published typebot',
|
||||
description: error.message,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
const { mutateAsync: updateTypebot, isLoading: isSaving } =
|
||||
trpc.typebot.updateTypebot.useMutation({
|
||||
onError: (error) =>
|
||||
showToast({
|
||||
title: 'Error while updating typebot',
|
||||
description: error.message,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
refetchTypebot()
|
||||
},
|
||||
})
|
||||
|
||||
const typebot = typebotData?.typebot
|
||||
const publishedTypebot = publishedTypebotData?.publishedTypebot ?? undefined
|
||||
|
||||
const [
|
||||
localTypebot,
|
||||
@@ -180,53 +192,17 @@ export const TypebotProvider = ({
|
||||
const typebotToSave = { ...localTypebot, ...updates }
|
||||
if (dequal(omit(typebot, 'updatedAt'), omit(typebotToSave, 'updatedAt')))
|
||||
return
|
||||
setIsSavingLoading(true)
|
||||
const { data, error } = await updateTypebotQuery(
|
||||
typebotToSave.id,
|
||||
typebotToSave
|
||||
)
|
||||
if (data?.typebot) setLocalTypebot({ ...data.typebot })
|
||||
setIsSavingLoading(false)
|
||||
if (error) {
|
||||
showToast({ title: error.name, description: error.message })
|
||||
return
|
||||
}
|
||||
mutate({
|
||||
setLocalTypebot({ ...typebotToSave })
|
||||
const { typebot: newTypebot } = await updateTypebot({
|
||||
typebotId: typebotToSave.id,
|
||||
typebot: typebotToSave,
|
||||
publishedTypebot,
|
||||
webhooks: webhooks ?? [],
|
||||
})
|
||||
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
setLocalTypebot({ ...newTypebot })
|
||||
return newTypebot
|
||||
},
|
||||
[
|
||||
localTypebot,
|
||||
mutate,
|
||||
publishedTypebot,
|
||||
setLocalTypebot,
|
||||
showToast,
|
||||
typebot,
|
||||
webhooks,
|
||||
]
|
||||
[localTypebot, setLocalTypebot, typebot, updateTypebot]
|
||||
)
|
||||
|
||||
const savePublishedTypebot = async (newPublishedTypebot: PublicTypebot) => {
|
||||
if (!localTypebot) return
|
||||
setIsPublishing(true)
|
||||
const { error } = await updatePublishedTypebotQuery(
|
||||
newPublishedTypebot.id,
|
||||
newPublishedTypebot,
|
||||
localTypebot.workspaceId
|
||||
)
|
||||
setIsPublishing(false)
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
||||
mutate({
|
||||
typebot: localTypebot,
|
||||
publishedTypebot: newPublishedTypebot,
|
||||
webhooks: webhooks ?? [],
|
||||
})
|
||||
}
|
||||
|
||||
useAutoSave(
|
||||
{
|
||||
handler: saveTypebot,
|
||||
@@ -246,12 +222,10 @@ export const TypebotProvider = ({
|
||||
}
|
||||
}, [saveTypebot])
|
||||
|
||||
const [isSavingLoading, setIsSavingLoading] = useState(false)
|
||||
const [isPublishing, setIsPublishing] = useState(false)
|
||||
|
||||
const isPublished = useMemo(
|
||||
() =>
|
||||
isDefined(localTypebot) &&
|
||||
isDefined(localTypebot.publicId) &&
|
||||
isDefined(publishedTypebot) &&
|
||||
isPublishedHelper(localTypebot, publishedTypebot),
|
||||
[localTypebot, publishedTypebot]
|
||||
@@ -268,56 +242,18 @@ export const TypebotProvider = ({
|
||||
}
|
||||
}, [localTypebot, typebot])
|
||||
|
||||
const updateLocalTypebot = (updates: UpdateTypebotPayload) =>
|
||||
localTypebot && setLocalTypebot({ ...localTypebot, ...updates })
|
||||
|
||||
const publishTypebot = async () => {
|
||||
const updateLocalTypebot = async ({
|
||||
updates,
|
||||
save,
|
||||
}: {
|
||||
updates: UpdateTypebotPayload
|
||||
save?: boolean
|
||||
}) => {
|
||||
if (!localTypebot) return
|
||||
const newLocalTypebot = { ...localTypebot }
|
||||
if (!publishedTypebot || !localTypebot.publicId) {
|
||||
const newPublicId =
|
||||
localTypebot.publicId ??
|
||||
parseDefaultPublicId(localTypebot.name, localTypebot.id)
|
||||
newLocalTypebot.publicId = newPublicId
|
||||
await saveTypebot({ publicId: newPublicId })
|
||||
}
|
||||
if (publishedTypebot) {
|
||||
await savePublishedTypebot({
|
||||
...convertTypebotToPublicTypebot(newLocalTypebot),
|
||||
id: publishedTypebot.id,
|
||||
})
|
||||
} else {
|
||||
setIsPublishing(true)
|
||||
const { data, error } = await createPublishedTypebotQuery(
|
||||
{
|
||||
...omit(convertTypebotToPublicTypebot(newLocalTypebot), 'id'),
|
||||
},
|
||||
localTypebot.workspaceId
|
||||
)
|
||||
setIsPublishing(false)
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
||||
mutate({
|
||||
typebot: localTypebot,
|
||||
publishedTypebot: data,
|
||||
webhooks: webhooks ?? [],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const unpublishTypebot = async () => {
|
||||
if (!publishedTypebot || !localTypebot) return
|
||||
setIsPublishing(true)
|
||||
const { error } = await deletePublishedTypebotQuery({
|
||||
publishedTypebotId: publishedTypebot.id,
|
||||
typebotId: localTypebot.id,
|
||||
})
|
||||
setIsPublishing(false)
|
||||
if (error) showToast({ description: error.message })
|
||||
mutate({
|
||||
typebot: localTypebot,
|
||||
webhooks: webhooks ?? [],
|
||||
})
|
||||
const newTypebot = { ...localTypebot, ...updates }
|
||||
setLocalTypebot(newTypebot)
|
||||
if (save) await saveTypebot(newTypebot)
|
||||
return newTypebot
|
||||
}
|
||||
|
||||
const restorePublishedTypebot = () => {
|
||||
@@ -325,64 +261,6 @@ export const TypebotProvider = ({
|
||||
setLocalTypebot(
|
||||
convertPublicTypebotToTypebot(publishedTypebot, localTypebot)
|
||||
)
|
||||
return saveTypebot()
|
||||
}
|
||||
|
||||
const updateWebhook = useCallback(
|
||||
async (webhookId: string, updates: Partial<Webhook>) => {
|
||||
if (!typebot) return
|
||||
const { data } = await updateWebhookQuery({
|
||||
typebotId: typebot.id,
|
||||
webhookId,
|
||||
data: updates,
|
||||
})
|
||||
if (data)
|
||||
mutate({
|
||||
typebot,
|
||||
publishedTypebot,
|
||||
webhooks: (webhooks ?? []).map((w) =>
|
||||
w.id === webhookId ? data.webhook : w
|
||||
),
|
||||
})
|
||||
},
|
||||
[mutate, publishedTypebot, typebot, webhooks]
|
||||
)
|
||||
|
||||
const createWebhook = async (data: Partial<Webhook>) => {
|
||||
if (!typebot) return
|
||||
const response = await createWebhookQuery({
|
||||
typebotId: typebot.id,
|
||||
data,
|
||||
})
|
||||
if (!response.data?.webhook) return
|
||||
mutate({
|
||||
typebot,
|
||||
publishedTypebot,
|
||||
webhooks: (webhooks ?? []).concat(response.data?.webhook),
|
||||
})
|
||||
}
|
||||
|
||||
const duplicateWebhook = async (
|
||||
existingWebhookId: string,
|
||||
newWebhookId: string
|
||||
) => {
|
||||
if (!typebot) return
|
||||
const newWebhook = await duplicateWebhookQuery({
|
||||
existingIds: {
|
||||
typebotId: typebot.id,
|
||||
webhookId: existingWebhookId,
|
||||
},
|
||||
newIds: {
|
||||
typebotId: typebot.id,
|
||||
webhookId: newWebhookId,
|
||||
},
|
||||
})
|
||||
if (!newWebhook) return
|
||||
mutate({
|
||||
typebot,
|
||||
publishedTypebot,
|
||||
webhooks: (webhooks ?? []).concat(newWebhook),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -391,29 +269,18 @@ export const TypebotProvider = ({
|
||||
typebot: localTypebot,
|
||||
publishedTypebot,
|
||||
linkedTypebots: linkedTypebotsData?.typebots ?? [],
|
||||
webhooks: webhooks ?? [],
|
||||
isReadOnly,
|
||||
isSavingLoading,
|
||||
isReadOnly: typebotData?.isReadOnly,
|
||||
isSavingLoading: isSaving,
|
||||
save: saveTypebot,
|
||||
undo,
|
||||
redo,
|
||||
canUndo,
|
||||
canRedo,
|
||||
publishTypebot,
|
||||
unpublishTypebot,
|
||||
isPublishing,
|
||||
isPublished,
|
||||
updateTypebot: updateLocalTypebot,
|
||||
restorePublishedTypebot,
|
||||
updateWebhook,
|
||||
...groupsActions(setLocalTypebot as SetTypebot, {
|
||||
onWebhookBlockCreated: createWebhook,
|
||||
onWebhookBlockDuplicated: duplicateWebhook,
|
||||
}),
|
||||
...blocksAction(setLocalTypebot as SetTypebot, {
|
||||
onWebhookBlockCreated: createWebhook,
|
||||
onWebhookBlockDuplicated: duplicateWebhook,
|
||||
}),
|
||||
...groupsActions(setLocalTypebot as SetTypebot),
|
||||
...blocksAction(setLocalTypebot as SetTypebot),
|
||||
...variablesAction(setLocalTypebot as SetTypebot),
|
||||
...edgesAction(setLocalTypebot as SetTypebot),
|
||||
...itemsAction(setLocalTypebot as SetTypebot),
|
||||
|
||||
@@ -37,10 +37,7 @@ export type WebhookCallBacks = {
|
||||
) => void
|
||||
}
|
||||
|
||||
export const blocksAction = (
|
||||
setTypebot: SetTypebot,
|
||||
{ onWebhookBlockCreated, onWebhookBlockDuplicated }: WebhookCallBacks
|
||||
): BlocksActions => ({
|
||||
export const blocksAction = (setTypebot: SetTypebot): BlocksActions => ({
|
||||
createBlock: (
|
||||
groupId: string,
|
||||
block: DraggableBlock | DraggableBlockType,
|
||||
@@ -48,13 +45,7 @@ export const blocksAction = (
|
||||
) =>
|
||||
setTypebot((typebot) =>
|
||||
produce(typebot, (typebot) => {
|
||||
createBlockDraft(
|
||||
typebot,
|
||||
block,
|
||||
groupId,
|
||||
indices,
|
||||
onWebhookBlockCreated
|
||||
)
|
||||
createBlockDraft(typebot, block, groupId, indices)
|
||||
})
|
||||
),
|
||||
updateBlock: (
|
||||
@@ -74,10 +65,7 @@ export const blocksAction = (
|
||||
const blocks = typebot.groups[groupIndex].blocks
|
||||
if (blockIndex === blocks.length - 1 && block.outgoingEdgeId)
|
||||
deleteEdgeDraft(typebot, block.outgoingEdgeId as string)
|
||||
const newBlock = duplicateBlockDraft(block.groupId)(
|
||||
block,
|
||||
onWebhookBlockDuplicated
|
||||
)
|
||||
const newBlock = duplicateBlockDraft(block.groupId)(block)
|
||||
typebot.groups[groupIndex].blocks.splice(blockIndex + 1, 0, newBlock)
|
||||
})
|
||||
),
|
||||
@@ -105,8 +93,7 @@ export const createBlockDraft = (
|
||||
typebot: Draft<Typebot>,
|
||||
block: DraggableBlock | DraggableBlockType,
|
||||
groupId: string,
|
||||
{ groupIndex, blockIndex }: BlockIndices,
|
||||
onWebhookBlockCreated?: (data: Partial<Webhook>) => void
|
||||
{ groupIndex, blockIndex }: BlockIndices
|
||||
) => {
|
||||
const blocks = typebot.groups[groupIndex].blocks
|
||||
if (
|
||||
@@ -116,13 +103,7 @@ export const createBlockDraft = (
|
||||
)
|
||||
deleteEdgeDraft(typebot, blocks[blockIndex - 1].outgoingEdgeId as string)
|
||||
typeof block === 'string'
|
||||
? createNewBlock(
|
||||
typebot,
|
||||
block,
|
||||
groupId,
|
||||
{ groupIndex, blockIndex },
|
||||
onWebhookBlockCreated
|
||||
)
|
||||
? createNewBlock(typebot, block, groupId, { groupIndex, blockIndex })
|
||||
: moveBlockToGroup(typebot, block, groupId, { groupIndex, blockIndex })
|
||||
removeEmptyGroups(typebot)
|
||||
}
|
||||
@@ -174,10 +155,7 @@ const moveBlockToGroup = (
|
||||
|
||||
export const duplicateBlockDraft =
|
||||
(groupId: string) =>
|
||||
(
|
||||
block: Block,
|
||||
onWebhookBlockDuplicated: WebhookCallBacks['onWebhookBlockDuplicated']
|
||||
): Block => {
|
||||
(block: Block): Block => {
|
||||
const blockId = createId()
|
||||
if (blockHasItems(block))
|
||||
return {
|
||||
@@ -189,8 +167,6 @@ export const duplicateBlockDraft =
|
||||
} as Block
|
||||
if (isWebhookBlock(block)) {
|
||||
const newWebhookId = createId()
|
||||
if (block.webhookId)
|
||||
onWebhookBlockDuplicated(block.webhookId, newWebhookId)
|
||||
return {
|
||||
...block,
|
||||
groupId,
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
deleteGroupDraft,
|
||||
createBlockDraft,
|
||||
duplicateBlockDraft,
|
||||
WebhookCallBacks,
|
||||
} from './blocks'
|
||||
import { isEmpty, parseGroupTitle } from '@typebot.io/lib'
|
||||
import { Coordinates } from '@/features/graph/types'
|
||||
@@ -29,10 +28,7 @@ export type GroupsActions = {
|
||||
deleteGroup: (groupIndex: number) => void
|
||||
}
|
||||
|
||||
const groupsActions = (
|
||||
setTypebot: SetTypebot,
|
||||
{ onWebhookBlockCreated, onWebhookBlockDuplicated }: WebhookCallBacks
|
||||
): GroupsActions => ({
|
||||
const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({
|
||||
createGroup: ({
|
||||
id,
|
||||
block,
|
||||
@@ -52,13 +48,7 @@ const groupsActions = (
|
||||
blocks: [],
|
||||
}
|
||||
typebot.groups.push(newGroup)
|
||||
createBlockDraft(
|
||||
typebot,
|
||||
block,
|
||||
newGroup.id,
|
||||
indices,
|
||||
onWebhookBlockCreated
|
||||
)
|
||||
createBlockDraft(typebot, block, newGroup.id, indices)
|
||||
})
|
||||
),
|
||||
updateGroup: (groupIndex: number, updates: Partial<Omit<Group, 'id'>>) =>
|
||||
@@ -79,9 +69,7 @@ const groupsActions = (
|
||||
? ''
|
||||
: `${parseGroupTitle(group.title)} copy`,
|
||||
id,
|
||||
blocks: group.blocks.map((block) =>
|
||||
duplicateBlockDraft(id)(block, onWebhookBlockDuplicated)
|
||||
),
|
||||
blocks: group.blocks.map((block) => duplicateBlockDraft(id)(block)),
|
||||
graphCoordinates: {
|
||||
x: group.graphCoordinates.x + 200,
|
||||
y: group.graphCoordinates.y + 100,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Typebot } from '@typebot.io/schemas'
|
||||
import { sendRequest } from '@typebot.io/lib'
|
||||
|
||||
export const updateTypebotQuery = async (id: string, typebot: Typebot) =>
|
||||
sendRequest<{ typebot: Typebot }>({
|
||||
url: `/api/typebots/${id}`,
|
||||
method: 'PUT',
|
||||
body: typebot,
|
||||
})
|
||||
@@ -24,8 +24,8 @@ import { useRouter } from 'next/router'
|
||||
import React, { useMemo } from 'react'
|
||||
import { deleteFolderQuery } from '../queries/deleteFolderQuery'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { updateFolderQuery } from '../queries/updateFolderQuery'
|
||||
import { useI18n, useScopedI18n } from '@/locales'
|
||||
import { updateFolderQuery } from '../queries/updateFolderQuery'
|
||||
|
||||
export const FolderButton = ({
|
||||
folder,
|
||||
|
||||
@@ -15,7 +15,6 @@ import { BackButton } from './BackButton'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { useFolders } from '../hooks/useFolders'
|
||||
import { patchTypebotQuery } from '../queries/patchTypebotQuery'
|
||||
import { createFolderQuery } from '../queries/createFolderQuery'
|
||||
import { CreateBotButton } from './CreateBotButton'
|
||||
import { CreateFolderButton } from './CreateFolderButton'
|
||||
@@ -25,6 +24,7 @@ import { TypebotCardOverlay } from './TypebotButtonOverlay'
|
||||
import { useI18n } from '@/locales'
|
||||
import { useTypebots } from '@/features/dashboard/hooks/useTypebots'
|
||||
import { TypebotInDashboard } from '@/features/dashboard/types'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
|
||||
type Props = { folder: DashboardFolder | null }
|
||||
|
||||
@@ -65,6 +65,15 @@ export const FolderContent = ({ folder }: Props) => {
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: updateTypebot } = trpc.typebot.updateTypebot.useMutation({
|
||||
onError: (error) => {
|
||||
showToast({ description: error.message })
|
||||
},
|
||||
onSuccess: () => {
|
||||
refetchTypebots()
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
typebots,
|
||||
isLoading: isTypebotLoading,
|
||||
@@ -81,11 +90,12 @@ export const FolderContent = ({ folder }: Props) => {
|
||||
|
||||
const moveTypebotToFolder = async (typebotId: string, folderId: string) => {
|
||||
if (!typebots) return
|
||||
const { error } = await patchTypebotQuery(typebotId, {
|
||||
folderId: folderId === 'root' ? null : folderId,
|
||||
updateTypebot({
|
||||
typebotId,
|
||||
typebot: {
|
||||
folderId: folderId === 'root' ? null : folderId,
|
||||
},
|
||||
})
|
||||
if (error) showToast({ description: error.message })
|
||||
refetchTypebots()
|
||||
}
|
||||
|
||||
const handleCreateFolder = async () => {
|
||||
|
||||
@@ -18,18 +18,13 @@ import { ConfirmModal } from '@/components/ConfirmModal'
|
||||
import { GripIcon } from '@/components/icons'
|
||||
import { useTypebotDnd } from '../TypebotDndProvider'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { Plan } from '@typebot.io/prisma'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { MoreButton } from './MoreButton'
|
||||
import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon'
|
||||
import { deletePublishedTypebotQuery } from '@/features/publish/queries/deletePublishedTypebotQuery'
|
||||
import { useScopedI18n } from '@/locales'
|
||||
import { deleteTypebotQuery } from '@/features/dashboard/queries/deleteTypebotQuery'
|
||||
import { getTypebotQuery } from '@/features/dashboard/queries/getTypebotQuery'
|
||||
import { importTypebotQuery } from '@/features/dashboard/queries/importTypebotQuery'
|
||||
import { TypebotInDashboard } from '@/features/dashboard/types'
|
||||
import { isMobile } from '@/helpers/isMobile'
|
||||
import { trpc, trpcVanilla } from '@/lib/trpc'
|
||||
|
||||
type Props = {
|
||||
typebot: TypebotInDashboard
|
||||
@@ -46,7 +41,6 @@ export const TypebotButton = ({
|
||||
}: Props) => {
|
||||
const scopedT = useScopedI18n('folders.typebotButton')
|
||||
const router = useRouter()
|
||||
const { workspace } = useWorkspace()
|
||||
const { draggedTypebot } = useTypebotDnd()
|
||||
const [draggedTypebotDebounced] = useDebounce(draggedTypebot, 200)
|
||||
const {
|
||||
@@ -57,6 +51,34 @@ export const TypebotButton = ({
|
||||
|
||||
const { showToast } = useToast()
|
||||
|
||||
const { mutate: createTypebot } = trpc.typebot.createTypebot.useMutation({
|
||||
onError: (error) => {
|
||||
showToast({ description: error.message })
|
||||
},
|
||||
onSuccess: ({ typebot }) => {
|
||||
router.push(`/typebots/${typebot.id}/edit`)
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: deleteTypebot } = trpc.typebot.deleteTypebot.useMutation({
|
||||
onError: (error) => {
|
||||
showToast({ description: error.message })
|
||||
},
|
||||
onSuccess: () => {
|
||||
onTypebotUpdated()
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: unpublishTypebot } =
|
||||
trpc.typebot.unpublishTypebot.useMutation({
|
||||
onError: (error) => {
|
||||
showToast({ description: error.message })
|
||||
},
|
||||
onSuccess: () => {
|
||||
onTypebotUpdated()
|
||||
},
|
||||
})
|
||||
|
||||
const handleTypebotClick = () => {
|
||||
if (draggedTypebotDebounced) return
|
||||
router.push(
|
||||
@@ -68,28 +90,27 @@ export const TypebotButton = ({
|
||||
|
||||
const handleDeleteTypebotClick = async () => {
|
||||
if (isReadOnly) return
|
||||
const { error } = await deleteTypebotQuery(typebot.id)
|
||||
if (error)
|
||||
return showToast({
|
||||
description: error.message,
|
||||
})
|
||||
onTypebotUpdated()
|
||||
deleteTypebot({
|
||||
typebotId: typebot.id,
|
||||
})
|
||||
}
|
||||
|
||||
const handleDuplicateClick = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
const { data } = await getTypebotQuery(typebot.id)
|
||||
const typebotToDuplicate = data?.typebot
|
||||
if (!typebotToDuplicate) return
|
||||
const { data: createdTypebot, error } = await importTypebotQuery(
|
||||
data.typebot,
|
||||
workspace?.plan ?? Plan.FREE
|
||||
)
|
||||
if (error)
|
||||
return showToast({
|
||||
description: error.message,
|
||||
const { typebot: typebotToDuplicate } =
|
||||
await trpcVanilla.typebot.getTypebot.query({
|
||||
typebotId: typebot.id,
|
||||
})
|
||||
if (createdTypebot) router.push(`/typebots/${createdTypebot?.id}/edit`)
|
||||
if (!typebotToDuplicate) return
|
||||
createTypebot({
|
||||
workspaceId: typebotToDuplicate.workspaceId,
|
||||
typebot: {
|
||||
...typebotToDuplicate,
|
||||
customDomain: undefined,
|
||||
publicId: undefined,
|
||||
name: duplicateName(typebotToDuplicate.name),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteClick = (e: React.MouseEvent) => {
|
||||
@@ -100,12 +121,7 @@ export const TypebotButton = ({
|
||||
const handleUnpublishClick = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (!typebot.publishedTypebotId) return
|
||||
const { error } = await deletePublishedTypebotQuery({
|
||||
publishedTypebotId: typebot.publishedTypebotId,
|
||||
typebotId: typebot.id,
|
||||
})
|
||||
if (error) showToast({ description: error.message })
|
||||
else onTypebotUpdated()
|
||||
unpublishTypebot({ typebotId: typebot.id })
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -205,3 +221,10 @@ export const TypebotButton = ({
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const duplicateName = (name: string | `${string} (${number})`) => {
|
||||
const match = name.match(/^(.*) \((\d+)\)$/)
|
||||
if (!match) return `${name} (1)`
|
||||
const [, nameWithoutNumber, number] = match
|
||||
return `${nameWithoutNumber} (${Number(number) + 1})`
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Typebot } from '@typebot.io/prisma'
|
||||
import { sendRequest } from '@typebot.io/lib'
|
||||
|
||||
export const patchTypebotQuery = async (
|
||||
id: string,
|
||||
typebot: Partial<Typebot>
|
||||
) =>
|
||||
sendRequest<{ typebot: Typebot }>({
|
||||
url: `/api/typebots/${id}`,
|
||||
method: 'PATCH',
|
||||
body: typebot,
|
||||
})
|
||||
@@ -117,9 +117,9 @@ export const ItemNode = ({
|
||||
source={{
|
||||
groupId: typebot.groups[indices.groupIndex].id,
|
||||
blockId:
|
||||
typebot.groups[indices.groupIndex].blocks[
|
||||
typebot.groups[indices.groupIndex]?.blocks[
|
||||
indices.blockIndex
|
||||
].id,
|
||||
]?.id,
|
||||
itemId: item.id,
|
||||
}}
|
||||
pos="absolute"
|
||||
|
||||
@@ -28,6 +28,9 @@ import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
|
||||
import { isFreePlan } from '@/features/billing/helpers/isFreePlan'
|
||||
import { parseTimeSince } from '@/helpers/parseTimeSince'
|
||||
import { useI18n } from '@/locales'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { parseDefaultPublicId } from '../helpers/parseDefaultPublicId'
|
||||
|
||||
export const PublishButton = (props: ButtonProps) => {
|
||||
const t = useI18n()
|
||||
@@ -36,36 +39,83 @@ export const PublishButton = (props: ButtonProps) => {
|
||||
const { push, query } = useRouter()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const {
|
||||
isPublishing,
|
||||
isPublished,
|
||||
publishTypebot,
|
||||
publishedTypebot,
|
||||
restorePublishedTypebot,
|
||||
typebot,
|
||||
isSavingLoading,
|
||||
updateTypebot,
|
||||
unpublishTypebot,
|
||||
save,
|
||||
} = useTypebot()
|
||||
const { showToast } = useToast()
|
||||
|
||||
const {
|
||||
typebot: {
|
||||
getPublishedTypebot: { refetch: refetchPublishedTypebot },
|
||||
},
|
||||
} = trpc.useContext()
|
||||
|
||||
const { mutate: publishTypebotMutate, isLoading: isPublishing } =
|
||||
trpc.typebot.publishTypebot.useMutation({
|
||||
onError: (error) =>
|
||||
showToast({
|
||||
title: 'Error while publishing typebot',
|
||||
description: error.message,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
if (!publishedTypebot) push(`/typebots/${query.typebotId}/share`)
|
||||
refetchPublishedTypebot()
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: unpublishTypebotMutate, isLoading: isUnpublishing } =
|
||||
trpc.typebot.unpublishTypebot.useMutation({
|
||||
onError: (error) =>
|
||||
showToast({
|
||||
title: 'Error while unpublishing typebot',
|
||||
description: error.message,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
if (!publishedTypebot) push(`/typebots/${query.typebotId}/share`)
|
||||
refetchPublishedTypebot()
|
||||
},
|
||||
})
|
||||
|
||||
const hasInputFile = typebot?.groups
|
||||
.flatMap((g) => g.blocks)
|
||||
.some((b) => b.type === InputBlockType.FILE)
|
||||
|
||||
const handlePublishClick = () => {
|
||||
const handlePublishClick = async () => {
|
||||
if (!typebot?.id) return
|
||||
if (isFreePlan(workspace) && hasInputFile) return onOpen()
|
||||
publishTypebot()
|
||||
if (!publishedTypebot) push(`/typebots/${query.typebotId}/share`)
|
||||
if (!typebot.publicId) {
|
||||
await updateTypebot({
|
||||
updates: {
|
||||
publicId: parseDefaultPublicId(typebot.name, typebot.id),
|
||||
},
|
||||
save: true,
|
||||
})
|
||||
} else await save()
|
||||
publishTypebotMutate({
|
||||
typebotId: typebot.id,
|
||||
})
|
||||
}
|
||||
|
||||
const unpublishTypebot = async () => {
|
||||
if (!typebot?.id) return
|
||||
if (typebot.isClosed)
|
||||
await updateTypebot({ updates: { isClosed: false }, save: true })
|
||||
unpublishTypebotMutate({
|
||||
typebotId: typebot?.id,
|
||||
})
|
||||
}
|
||||
|
||||
const closeTypebot = async () => {
|
||||
updateTypebot({ isClosed: true })
|
||||
await save()
|
||||
await updateTypebot({ updates: { isClosed: true }, save: true })
|
||||
}
|
||||
|
||||
const openTypebot = async () => {
|
||||
updateTypebot({ isClosed: false })
|
||||
await save()
|
||||
await updateTypebot({ updates: { isClosed: false }, save: true })
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -99,8 +149,8 @@ export const PublishButton = (props: ButtonProps) => {
|
||||
>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
isLoading={isPublishing || isSavingLoading}
|
||||
isDisabled={isPublished}
|
||||
isLoading={isPublishing || isUnpublishing}
|
||||
isDisabled={isPublished || isSavingLoading}
|
||||
onClick={handlePublishClick}
|
||||
borderRightRadius={publishedTypebot ? 0 : undefined}
|
||||
{...props}
|
||||
|
||||
@@ -33,7 +33,7 @@ export const SharePage = () => {
|
||||
const { showToast } = useToast()
|
||||
|
||||
const handlePublicIdChange = async (publicId: string) => {
|
||||
updateTypebot({ publicId })
|
||||
updateTypebot({ updates: { publicId } })
|
||||
}
|
||||
|
||||
const publicId = typebot
|
||||
@@ -50,7 +50,7 @@ export const SharePage = () => {
|
||||
}
|
||||
|
||||
const handleCustomDomainChange = (customDomain: string | null) =>
|
||||
updateTypebot({ customDomain })
|
||||
updateTypebot({ updates: { customDomain } })
|
||||
|
||||
const checkIfPathnameIsValid = (pathname: string) => {
|
||||
const isCorrectlyFormatted =
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { PublicTypebot } from '@typebot.io/schemas'
|
||||
import { sendRequest } from '@typebot.io/lib'
|
||||
|
||||
export const createPublishedTypebotQuery = async (
|
||||
typebot: Omit<PublicTypebot, 'id'>,
|
||||
workspaceId: string
|
||||
) =>
|
||||
sendRequest<PublicTypebot>({
|
||||
url: `/api/publicTypebots?workspaceId=${workspaceId}`,
|
||||
method: 'POST',
|
||||
body: typebot,
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
import { sendRequest } from '@typebot.io/lib'
|
||||
|
||||
export const deletePublishedTypebotQuery = ({
|
||||
publishedTypebotId,
|
||||
typebotId,
|
||||
}: {
|
||||
publishedTypebotId: string
|
||||
typebotId: string
|
||||
}) =>
|
||||
sendRequest({
|
||||
method: 'DELETE',
|
||||
url: `/api/publicTypebots/${publishedTypebotId}?typebotId=${typebotId}`,
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
import { PublicTypebot } from '@typebot.io/schemas'
|
||||
import { sendRequest } from '@typebot.io/lib'
|
||||
|
||||
export const updatePublishedTypebotQuery = async (
|
||||
id: string,
|
||||
typebot: Omit<PublicTypebot, 'id'>,
|
||||
workspaceId: string
|
||||
) =>
|
||||
sendRequest({
|
||||
url: `/api/publicTypebots/${id}?workspaceId=${workspaceId}`,
|
||||
method: 'PUT',
|
||||
body: typebot,
|
||||
})
|
||||
@@ -1,10 +1,10 @@
|
||||
import { getTypebot } from '@/features/typebot/helpers/getTypebot'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { Typebot } from '@typebot.io/schemas'
|
||||
import { Group } from '@typebot.io/schemas'
|
||||
import { z } from 'zod'
|
||||
import { archiveResults } from '@typebot.io/lib/api/helpers/archiveResults'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { isWriteTypebotForbidden } from '@/features/typebot/helpers/isWriteTypebotForbidden'
|
||||
|
||||
export const deleteResults = authenticatedProcedure
|
||||
.meta({
|
||||
@@ -31,18 +31,27 @@ export const deleteResults = authenticatedProcedure
|
||||
.mutation(async ({ input, ctx: { user } }) => {
|
||||
const idsArray = input.resultIds?.split(',')
|
||||
const { typebotId } = input
|
||||
const typebot = (await getTypebot({
|
||||
accessLevel: 'write',
|
||||
typebotId,
|
||||
user,
|
||||
select: {
|
||||
groups: true,
|
||||
const typebot = await prisma.typebot.findUnique({
|
||||
where: {
|
||||
id: typebotId,
|
||||
},
|
||||
})) as Pick<Typebot, 'groups'> | null
|
||||
if (!typebot)
|
||||
select: {
|
||||
workspaceId: true,
|
||||
groups: true,
|
||||
collaborators: {
|
||||
select: {
|
||||
userId: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!typebot || (await isWriteTypebotForbidden(typebot, user)))
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
|
||||
const { success } = await archiveResults(prisma)({
|
||||
typebot,
|
||||
typebot: {
|
||||
groups: typebot.groups as Group[],
|
||||
},
|
||||
resultsFilter: {
|
||||
id: (idsArray?.length ?? 0) > 0 ? { in: idsArray } : undefined,
|
||||
typebotId,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { getTypebot } from '@/features/typebot/helpers/getTypebot'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { ResultWithAnswers, resultWithAnswersSchema } from '@typebot.io/schemas'
|
||||
import { z } from 'zod'
|
||||
import { isReadTypebotForbidden } from '@/features/typebot/helpers/isReadTypebotForbidden'
|
||||
|
||||
export const getResult = authenticatedProcedure
|
||||
.meta({
|
||||
@@ -27,12 +27,23 @@ export const getResult = authenticatedProcedure
|
||||
})
|
||||
)
|
||||
.query(async ({ input, ctx: { user } }) => {
|
||||
const typebot = await getTypebot({
|
||||
accessLevel: 'read',
|
||||
user,
|
||||
typebotId: input.typebotId,
|
||||
const typebot = await prisma.typebot.findUnique({
|
||||
where: {
|
||||
id: input.typebotId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
workspaceId: true,
|
||||
groups: true,
|
||||
collaborators: {
|
||||
select: {
|
||||
userId: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!typebot?.id)
|
||||
if (!typebot || (await isReadTypebotForbidden(typebot, user)))
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
|
||||
const results = (await prisma.result.findMany({
|
||||
where: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getTypebot } from '@/features/typebot/helpers/getTypebot'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import { logSchema } from '@typebot.io/schemas'
|
||||
import { z } from 'zod'
|
||||
import { isReadTypebotForbidden } from '@/features/typebot/helpers/isReadTypebotForbidden'
|
||||
|
||||
export const getResultLogs = authenticatedProcedure
|
||||
.meta({
|
||||
@@ -22,12 +22,24 @@ export const getResultLogs = authenticatedProcedure
|
||||
)
|
||||
.output(z.object({ logs: z.array(logSchema) }))
|
||||
.query(async ({ input: { typebotId, resultId }, ctx: { user } }) => {
|
||||
const typebot = await getTypebot({
|
||||
accessLevel: 'read',
|
||||
user,
|
||||
typebotId,
|
||||
const typebot = await prisma.typebot.findUnique({
|
||||
where: {
|
||||
id: typebotId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
workspaceId: true,
|
||||
groups: true,
|
||||
collaborators: {
|
||||
select: {
|
||||
userId: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!typebot) throw new Error('Typebot not found')
|
||||
if (!typebot || (await isReadTypebotForbidden(typebot, user)))
|
||||
throw new Error('Typebot not found')
|
||||
const logs = await prisma.log.findMany({
|
||||
where: {
|
||||
resultId,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { getTypebot } from '@/features/typebot/helpers/getTypebot'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { ResultWithAnswers, resultWithAnswersSchema } from '@typebot.io/schemas'
|
||||
import { z } from 'zod'
|
||||
import { isReadTypebotForbidden } from '@/features/typebot/helpers/isReadTypebotForbidden'
|
||||
|
||||
const maxLimit = 200
|
||||
|
||||
@@ -38,12 +38,23 @@ export const getResults = authenticatedProcedure
|
||||
message: 'limit must be between 1 and 200',
|
||||
})
|
||||
const { cursor } = input
|
||||
const typebot = await getTypebot({
|
||||
accessLevel: 'read',
|
||||
user,
|
||||
typebotId: input.typebotId,
|
||||
const typebot = await prisma.typebot.findUnique({
|
||||
where: {
|
||||
id: input.typebotId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
workspaceId: true,
|
||||
groups: true,
|
||||
collaborators: {
|
||||
select: {
|
||||
userId: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!typebot?.id)
|
||||
if (!typebot || (await isReadTypebotForbidden(typebot, user)))
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
|
||||
const results = (await prisma.result.findMany({
|
||||
take: limit + 1,
|
||||
|
||||
@@ -67,10 +67,12 @@ export const ResultsTable = ({
|
||||
const changeColumnOrder = (newColumnOrder: string[]) => {
|
||||
if (typeof newColumnOrder === 'function') return
|
||||
updateTypebot({
|
||||
resultsTablePreferences: {
|
||||
columnsOrder: newColumnOrder,
|
||||
columnsVisibility,
|
||||
columnsWidth,
|
||||
updates: {
|
||||
resultsTablePreferences: {
|
||||
columnsOrder: newColumnOrder,
|
||||
columnsVisibility,
|
||||
columnsWidth,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -80,10 +82,12 @@ export const ResultsTable = ({
|
||||
) => {
|
||||
if (typeof newColumnVisibility === 'function') return
|
||||
updateTypebot({
|
||||
resultsTablePreferences: {
|
||||
columnsVisibility: newColumnVisibility,
|
||||
columnsWidth,
|
||||
columnsOrder,
|
||||
updates: {
|
||||
resultsTablePreferences: {
|
||||
columnsVisibility: newColumnVisibility,
|
||||
columnsWidth,
|
||||
columnsOrder,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -93,10 +97,12 @@ export const ResultsTable = ({
|
||||
) => {
|
||||
if (typeof newColumnSizing === 'object') return
|
||||
updateTypebot({
|
||||
resultsTablePreferences: {
|
||||
columnsWidth: newColumnSizing(columnsWidth),
|
||||
columnsVisibility,
|
||||
columnsOrder,
|
||||
updates: {
|
||||
resultsTablePreferences: {
|
||||
columnsWidth: newColumnSizing(columnsWidth),
|
||||
columnsVisibility,
|
||||
columnsOrder,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,13 +22,17 @@ export const SettingsSideMenu = () => {
|
||||
|
||||
const handleTypingEmulationChange = (typingEmulation: TypingEmulation) =>
|
||||
typebot &&
|
||||
updateTypebot({ settings: { ...typebot.settings, typingEmulation } })
|
||||
updateTypebot({
|
||||
updates: { settings: { ...typebot.settings, typingEmulation } },
|
||||
})
|
||||
|
||||
const handleGeneralSettingsChange = (general: GeneralSettings) =>
|
||||
typebot && updateTypebot({ settings: { ...typebot.settings, general } })
|
||||
typebot &&
|
||||
updateTypebot({ updates: { settings: { ...typebot.settings, general } } })
|
||||
|
||||
const handleMetadataChange = (metadata: Metadata) =>
|
||||
typebot && updateTypebot({ settings: { ...typebot.settings, metadata } })
|
||||
typebot &&
|
||||
updateTypebot({ updates: { settings: { ...typebot.settings, metadata } } })
|
||||
|
||||
return (
|
||||
<Stack
|
||||
|
||||
@@ -12,7 +12,7 @@ test.describe.parallel('Settings page', () => {
|
||||
id: typebotId,
|
||||
})
|
||||
await page.goto(`/typebots/${typebotId}/settings`)
|
||||
|
||||
|
||||
await page.click('text="Remember user"')
|
||||
|
||||
await expect(page.getByPlaceholder('Type your answer...')).toHaveValue(
|
||||
|
||||
@@ -15,8 +15,7 @@ import { TemplatesModal } from './TemplatesModal'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
import { useUser } from '@/features/account/hooks/useUser'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { createTypebotQuery } from '@/features/dashboard/queries/createTypebotQuery'
|
||||
import { importTypebotQuery } from '@/features/dashboard/queries/importTypebotQuery'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
|
||||
export const CreateNewTypebotButtons = () => {
|
||||
const { workspace } = useWorkspace()
|
||||
@@ -28,40 +27,16 @@ export const CreateNewTypebotButtons = () => {
|
||||
|
||||
const { showToast } = useToast()
|
||||
|
||||
const handleCreateSubmit = async (typebot?: Typebot) => {
|
||||
if (!user || !workspace) return
|
||||
setIsLoading(true)
|
||||
const folderId = router.query.folderId?.toString() ?? null
|
||||
const { error, data } = typebot
|
||||
? await importTypebotQuery(
|
||||
{
|
||||
...typebot,
|
||||
folderId,
|
||||
workspaceId: workspace.id,
|
||||
theme: {
|
||||
...typebot.theme,
|
||||
chat: {
|
||||
...typebot.theme.chat,
|
||||
hostAvatar: {
|
||||
isEnabled: true,
|
||||
url:
|
||||
typebot.theme.chat.hostAvatar?.url ??
|
||||
user.image ??
|
||||
undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
workspace.plan
|
||||
)
|
||||
: await createTypebotQuery({
|
||||
folderId,
|
||||
workspaceId: workspace.id,
|
||||
})
|
||||
if (error) showToast({ description: error.message })
|
||||
if (data)
|
||||
const { mutate } = trpc.typebot.createTypebot.useMutation({
|
||||
onMutate: () => {
|
||||
setIsLoading(true)
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast({ description: error.message })
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
router.push({
|
||||
pathname: `/typebots/${data.id}/edit`,
|
||||
pathname: `/typebots/${data.typebot.id}/edit`,
|
||||
query:
|
||||
router.query.isFirstBot === 'true'
|
||||
? {
|
||||
@@ -69,7 +44,22 @@ export const CreateNewTypebotButtons = () => {
|
||||
}
|
||||
: {},
|
||||
})
|
||||
setIsLoading(false)
|
||||
},
|
||||
onSettled: () => {
|
||||
setIsLoading(false)
|
||||
},
|
||||
})
|
||||
|
||||
const handleCreateSubmit = async (typebot?: Typebot) => {
|
||||
if (!user || !workspace) return
|
||||
const folderId = router.query.folderId?.toString() ?? null
|
||||
mutate({
|
||||
workspaceId: workspace.id,
|
||||
typebot: {
|
||||
...(typebot ? { ...typebot } : {}),
|
||||
folderId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { parseInvalidTypebot } from '@/features/typebot/helpers/parseInvalidTypebot'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { Button, ButtonProps, chakra } from '@chakra-ui/react'
|
||||
import { groupSchema, Typebot } from '@typebot.io/schemas'
|
||||
import { Typebot, typebotSchema } from '@typebot.io/schemas'
|
||||
import React, { ChangeEvent } from 'react'
|
||||
import { z } from 'zod'
|
||||
|
||||
type Props = {
|
||||
onNewTypebot: (typebot: Typebot) => void
|
||||
@@ -19,8 +19,13 @@ export const ImportTypebotFromFileButton = ({
|
||||
const file = e.target.files[0]
|
||||
const fileContent = await readFile(file)
|
||||
try {
|
||||
const typebot = JSON.parse(fileContent)
|
||||
z.array(groupSchema).parse(typebot.groups)
|
||||
const typebot = parseInvalidTypebot(JSON.parse(fileContent))
|
||||
typebotSchema
|
||||
.omit({
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
})
|
||||
.parse(typebot)
|
||||
onNewTypebot(typebot)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
|
||||
@@ -23,13 +23,15 @@ export const ThemeSideMenu = () => {
|
||||
const { typebot, updateTypebot } = useTypebot()
|
||||
|
||||
const updateChatTheme = (chat: ChatTheme) =>
|
||||
typebot && updateTypebot({ theme: { ...typebot.theme, chat } })
|
||||
typebot && updateTypebot({ updates: { theme: { ...typebot.theme, chat } } })
|
||||
|
||||
const updateGeneralTheme = (general: GeneralTheme) =>
|
||||
typebot && updateTypebot({ theme: { ...typebot.theme, general } })
|
||||
typebot &&
|
||||
updateTypebot({ updates: { theme: { ...typebot.theme, general } } })
|
||||
|
||||
const updateCustomCss = (customCss: string) =>
|
||||
typebot && updateTypebot({ theme: { ...typebot.theme, customCss } })
|
||||
typebot &&
|
||||
updateTypebot({ updates: { theme: { ...typebot.theme, customCss } } })
|
||||
|
||||
const selectedTemplate = (
|
||||
selectedTemplate: Partial<Pick<ThemeTemplate, 'id' | 'theme'>>
|
||||
@@ -37,15 +39,19 @@ export const ThemeSideMenu = () => {
|
||||
if (!typebot) return
|
||||
const { theme, id } = selectedTemplate
|
||||
updateTypebot({
|
||||
selectedThemeTemplateId: id,
|
||||
theme: theme ? { ...theme } : typebot.theme,
|
||||
updates: {
|
||||
selectedThemeTemplateId: id,
|
||||
theme: theme ? { ...theme } : typebot.theme,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const updateBranding = (isBrandingEnabled: boolean) =>
|
||||
typebot &&
|
||||
updateTypebot({
|
||||
settings: { ...typebot.settings, general: { isBrandingEnabled } },
|
||||
updates: {
|
||||
settings: { ...typebot.settings, general: { isBrandingEnabled } },
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
129
apps/builder/src/features/typebot/api/createTypebot.ts
Normal file
129
apps/builder/src/features/typebot/api/createTypebot.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { Plan, WorkspaceRole } from '@typebot.io/prisma'
|
||||
import {
|
||||
defaultSettings,
|
||||
defaultTheme,
|
||||
typebotSchema,
|
||||
} from '@typebot.io/schemas'
|
||||
import { z } from 'zod'
|
||||
import { getUserRoleInWorkspace } from '@/features/workspace/helpers/getUserRoleInWorkspace'
|
||||
import {
|
||||
isCustomDomainNotAvailable,
|
||||
isPublicIdNotAvailable,
|
||||
sanitizeGroups,
|
||||
sanitizeSettings,
|
||||
} from '../helpers/sanitizers'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
|
||||
export const createTypebot = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/typebots',
|
||||
protect: true,
|
||||
summary: 'Create a typebot',
|
||||
tags: ['Typebot'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
workspaceId: z.string(),
|
||||
typebot: typebotSchema
|
||||
.pick({
|
||||
name: true,
|
||||
icon: true,
|
||||
selectedThemeTemplateId: true,
|
||||
groups: true,
|
||||
theme: true,
|
||||
settings: true,
|
||||
folderId: true,
|
||||
variables: true,
|
||||
edges: true,
|
||||
resultsTablePreferences: true,
|
||||
publicId: true,
|
||||
customDomain: true,
|
||||
})
|
||||
.partial(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
typebot: typebotSchema,
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input: { typebot, workspaceId }, ctx: { user } }) => {
|
||||
const workspace = await prisma.workspace.findUnique({
|
||||
where: { id: workspaceId },
|
||||
select: { id: true, members: true, plan: true },
|
||||
})
|
||||
const userRole = getUserRoleInWorkspace(user.id, workspace?.members)
|
||||
if (
|
||||
userRole === undefined ||
|
||||
userRole === WorkspaceRole.GUEST ||
|
||||
!workspace
|
||||
)
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
|
||||
|
||||
if (
|
||||
typebot.customDomain &&
|
||||
(await isCustomDomainNotAvailable(typebot.customDomain))
|
||||
)
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Custom domain not available',
|
||||
})
|
||||
|
||||
if (typebot.publicId && (await isPublicIdNotAvailable(typebot.publicId)))
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Public id not available',
|
||||
})
|
||||
|
||||
const newTypebot = await prisma.typebot.create({
|
||||
data: {
|
||||
version: '5',
|
||||
workspaceId,
|
||||
name: typebot.name ?? 'My typebot',
|
||||
icon: typebot.icon,
|
||||
selectedThemeTemplateId: typebot.selectedThemeTemplateId,
|
||||
groups: typebot.groups
|
||||
? await sanitizeGroups(workspaceId)(typebot.groups)
|
||||
: defaultGroups(),
|
||||
theme: typebot.theme ? typebot.theme : defaultTheme,
|
||||
settings: typebot.settings
|
||||
? sanitizeSettings(typebot.settings, workspace.plan)
|
||||
: defaultSettings({
|
||||
isBrandingEnabled: workspace.plan === Plan.FREE,
|
||||
}),
|
||||
folderId: typebot.folderId,
|
||||
variables: typebot.variables ?? [],
|
||||
edges: typebot.edges ?? [],
|
||||
resultsTablePreferences: typebot.resultsTablePreferences ?? undefined,
|
||||
publicId: typebot.publicId ?? undefined,
|
||||
customDomain: typebot.customDomain ?? undefined,
|
||||
},
|
||||
})
|
||||
|
||||
return { typebot: typebotSchema.parse(newTypebot) }
|
||||
})
|
||||
|
||||
const defaultGroups = () => {
|
||||
const groupId = createId()
|
||||
return [
|
||||
{
|
||||
id: groupId,
|
||||
title: 'Start',
|
||||
graphCoordinates: { x: 0, y: 0 },
|
||||
blocks: [
|
||||
{
|
||||
groupId,
|
||||
id: createId(),
|
||||
label: 'Start',
|
||||
type: 'start',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
73
apps/builder/src/features/typebot/api/deleteTypebot.ts
Normal file
73
apps/builder/src/features/typebot/api/deleteTypebot.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { Group } from '@typebot.io/schemas'
|
||||
import { z } from 'zod'
|
||||
import { isWriteTypebotForbidden } from '../helpers/isWriteTypebotForbidden'
|
||||
import { archiveResults } from '@typebot.io/lib/api/helpers/archiveResults'
|
||||
|
||||
export const deleteTypebot = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'DELETE',
|
||||
path: '/typebots/{typebotId}',
|
||||
protect: true,
|
||||
summary: 'Delete a typebot',
|
||||
tags: ['Typebot'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
typebotId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
message: z.literal('success'),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input: { typebotId }, ctx: { user } }) => {
|
||||
const existingTypebot = await prisma.typebot.findFirst({
|
||||
where: {
|
||||
id: typebotId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
workspaceId: true,
|
||||
groups: true,
|
||||
collaborators: {
|
||||
select: {
|
||||
userId: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if (
|
||||
!existingTypebot?.id ||
|
||||
(await isWriteTypebotForbidden(existingTypebot, user))
|
||||
)
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
|
||||
|
||||
const { success } = await archiveResults(prisma)({
|
||||
typebot: {
|
||||
groups: existingTypebot.groups as Group[],
|
||||
},
|
||||
resultsFilter: { typebotId },
|
||||
})
|
||||
if (!success)
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Failed to archive results',
|
||||
})
|
||||
await prisma.publicTypebot.deleteMany({
|
||||
where: { typebotId },
|
||||
})
|
||||
await prisma.typebot.updateMany({
|
||||
where: { id: typebotId },
|
||||
data: { isArchived: true, publicId: null, customDomain: null },
|
||||
})
|
||||
return {
|
||||
message: 'success',
|
||||
}
|
||||
})
|
||||
64
apps/builder/src/features/typebot/api/getPublishedTypebot.ts
Normal file
64
apps/builder/src/features/typebot/api/getPublishedTypebot.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { publicTypebotSchema } from '@typebot.io/schemas'
|
||||
import { z } from 'zod'
|
||||
import { isReadTypebotForbidden } from '../helpers/isReadTypebotForbidden'
|
||||
|
||||
export const getPublishedTypebot = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/typebots/{typebotId}/publishedTypebot',
|
||||
protect: true,
|
||||
summary: 'Get published typebot',
|
||||
tags: ['Typebot'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
typebotId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
publishedTypebot: publicTypebotSchema.nullable(),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { typebotId }, ctx: { user } }) => {
|
||||
const existingTypebot = await prisma.typebot.findFirst({
|
||||
where: {
|
||||
id: typebotId,
|
||||
},
|
||||
include: {
|
||||
collaborators: true,
|
||||
publishedTypebot: true,
|
||||
},
|
||||
})
|
||||
if (
|
||||
!existingTypebot?.id ||
|
||||
(await isReadTypebotForbidden(existingTypebot, user))
|
||||
)
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
|
||||
|
||||
if (!existingTypebot.publishedTypebot)
|
||||
return {
|
||||
publishedTypebot: null,
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedTypebot = publicTypebotSchema.parse(
|
||||
existingTypebot.publishedTypebot
|
||||
)
|
||||
|
||||
return {
|
||||
publishedTypebot: parsedTypebot,
|
||||
}
|
||||
} catch (err) {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Failed to parse published typebot',
|
||||
cause: err,
|
||||
})
|
||||
}
|
||||
})
|
||||
75
apps/builder/src/features/typebot/api/getTypebot.ts
Normal file
75
apps/builder/src/features/typebot/api/getTypebot.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { Typebot, typebotSchema } from '@typebot.io/schemas'
|
||||
import { z } from 'zod'
|
||||
import { isReadTypebotForbidden } from '../helpers/isReadTypebotForbidden'
|
||||
import { omit } from '@typebot.io/lib'
|
||||
import { Typebot as TypebotFromDb } from '@typebot.io/prisma'
|
||||
import { migrateTypebotFromV3ToV4 } from '@typebot.io/lib/migrations/migrateTypebotFromV3ToV4'
|
||||
import { parseInvalidTypebot } from '../helpers/parseInvalidTypebot'
|
||||
|
||||
export const getTypebot = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/typebots/{typebotId}',
|
||||
protect: true,
|
||||
summary: 'Get a typebot',
|
||||
tags: ['Typebot'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
typebotId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
typebot: typebotSchema,
|
||||
isReadOnly: z.boolean(),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { typebotId }, ctx: { user } }) => {
|
||||
const existingTypebot = await prisma.typebot.findFirst({
|
||||
where: {
|
||||
id: typebotId,
|
||||
},
|
||||
include: {
|
||||
collaborators: true,
|
||||
},
|
||||
})
|
||||
if (
|
||||
!existingTypebot?.id ||
|
||||
(await isReadTypebotForbidden(existingTypebot, user))
|
||||
)
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
|
||||
|
||||
try {
|
||||
const parsedTypebot = await parseTypebot(
|
||||
omit(existingTypebot, 'collaborators')
|
||||
)
|
||||
|
||||
return {
|
||||
typebot: parsedTypebot,
|
||||
isReadOnly:
|
||||
existingTypebot.collaborators.find(
|
||||
(collaborator) => collaborator.userId === user.id
|
||||
)?.type === 'READ' ?? false,
|
||||
}
|
||||
} catch (err) {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Failed to parse typebot',
|
||||
cause: err,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const parseTypebot = async (typebot: TypebotFromDb): Promise<Typebot> => {
|
||||
const parsedTypebot = typebotSchema.parse(
|
||||
typebot.version !== '5' ? parseInvalidTypebot(typebot as Typebot) : typebot
|
||||
)
|
||||
if (['4', '5'].includes(parsedTypebot.version ?? '')) return parsedTypebot
|
||||
return migrateTypebotFromV3ToV4(prisma)(parsedTypebot)
|
||||
}
|
||||
80
apps/builder/src/features/typebot/api/publishTypebot.ts
Normal file
80
apps/builder/src/features/typebot/api/publishTypebot.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { typebotSchema } from '@typebot.io/schemas'
|
||||
import { z } from 'zod'
|
||||
import { isWriteTypebotForbidden } from '../helpers/isWriteTypebotForbidden'
|
||||
|
||||
export const publishTypebot = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/typebots/{typebotId}/publish',
|
||||
protect: true,
|
||||
summary: 'Publish a typebot',
|
||||
tags: ['Typebot'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
typebotId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
message: z.literal('success'),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input: { typebotId }, ctx: { user } }) => {
|
||||
const existingTypebot = await prisma.typebot.findFirst({
|
||||
where: {
|
||||
id: typebotId,
|
||||
},
|
||||
include: {
|
||||
collaborators: true,
|
||||
publishedTypebot: true,
|
||||
},
|
||||
})
|
||||
if (
|
||||
!existingTypebot?.id ||
|
||||
(await isWriteTypebotForbidden(existingTypebot, user))
|
||||
)
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
|
||||
|
||||
if (existingTypebot.publishedTypebot) {
|
||||
await prisma.publicTypebot.updateMany({
|
||||
where: {
|
||||
id: existingTypebot.publishedTypebot.id,
|
||||
},
|
||||
data: {
|
||||
version: existingTypebot.version,
|
||||
edges: typebotSchema.shape.edges.parse(existingTypebot.edges),
|
||||
groups: typebotSchema.shape.groups.parse(existingTypebot.groups),
|
||||
settings: typebotSchema.shape.settings.parse(
|
||||
existingTypebot.settings
|
||||
),
|
||||
variables: typebotSchema.shape.variables.parse(
|
||||
existingTypebot.variables
|
||||
),
|
||||
theme: typebotSchema.shape.theme.parse(existingTypebot.theme),
|
||||
},
|
||||
})
|
||||
return { message: 'success' }
|
||||
}
|
||||
|
||||
await prisma.publicTypebot.createMany({
|
||||
data: {
|
||||
version: existingTypebot.version,
|
||||
typebotId: existingTypebot.id,
|
||||
edges: typebotSchema.shape.edges.parse(existingTypebot.edges),
|
||||
groups: typebotSchema.shape.groups.parse(existingTypebot.groups),
|
||||
settings: typebotSchema.shape.settings.parse(existingTypebot.settings),
|
||||
variables: typebotSchema.shape.variables.parse(
|
||||
existingTypebot.variables
|
||||
),
|
||||
theme: typebotSchema.shape.theme.parse(existingTypebot.theme),
|
||||
},
|
||||
})
|
||||
|
||||
return { message: 'success' }
|
||||
})
|
||||
@@ -1,6 +1,20 @@
|
||||
import { router } from '@/helpers/server/trpc'
|
||||
import { listTypebots } from './listTypebots'
|
||||
import { createTypebot } from './createTypebot'
|
||||
import { updateTypebot } from './updateTypebot'
|
||||
import { getTypebot } from './getTypebot'
|
||||
import { getPublishedTypebot } from './getPublishedTypebot'
|
||||
import { publishTypebot } from './publishTypebot'
|
||||
import { unpublishTypebot } from './unpublishTypebot'
|
||||
import { deleteTypebot } from './deleteTypebot'
|
||||
|
||||
export const typebotRouter = router({
|
||||
createTypebot,
|
||||
updateTypebot,
|
||||
getTypebot,
|
||||
getPublishedTypebot,
|
||||
publishTypebot,
|
||||
unpublishTypebot,
|
||||
listTypebots,
|
||||
deleteTypebot,
|
||||
})
|
||||
|
||||
56
apps/builder/src/features/typebot/api/unpublishTypebot.ts
Normal file
56
apps/builder/src/features/typebot/api/unpublishTypebot.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { z } from 'zod'
|
||||
import { isWriteTypebotForbidden } from '../helpers/isWriteTypebotForbidden'
|
||||
|
||||
export const unpublishTypebot = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'POST',
|
||||
path: '/typebots/{typebotId}/unpublish',
|
||||
protect: true,
|
||||
summary: 'Unpublish a typebot',
|
||||
tags: ['Typebot'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
typebotId: z.string(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
message: z.literal('success'),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input: { typebotId }, ctx: { user } }) => {
|
||||
const existingTypebot = await prisma.typebot.findFirst({
|
||||
where: {
|
||||
id: typebotId,
|
||||
},
|
||||
include: {
|
||||
collaborators: true,
|
||||
publishedTypebot: true,
|
||||
},
|
||||
})
|
||||
if (!existingTypebot?.publishedTypebot)
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Published typebot not found',
|
||||
})
|
||||
|
||||
if (
|
||||
!existingTypebot.id ||
|
||||
(await isWriteTypebotForbidden(existingTypebot, user))
|
||||
)
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
|
||||
|
||||
await prisma.publicTypebot.deleteMany({
|
||||
where: {
|
||||
id: existingTypebot.publishedTypebot.id,
|
||||
},
|
||||
})
|
||||
|
||||
return { message: 'success' }
|
||||
})
|
||||
163
apps/builder/src/features/typebot/api/updateTypebot.ts
Normal file
163
apps/builder/src/features/typebot/api/updateTypebot.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import {
|
||||
defaultSettings,
|
||||
defaultTheme,
|
||||
typebotSchema,
|
||||
} from '@typebot.io/schemas'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
isCustomDomainNotAvailable,
|
||||
isPublicIdNotAvailable,
|
||||
sanitizeGroups,
|
||||
sanitizeSettings,
|
||||
} from '../helpers/sanitizers'
|
||||
import { isWriteTypebotForbidden } from '../helpers/isWriteTypebotForbidden'
|
||||
import { isCloudProdInstance } from '@/helpers/isCloudProdInstance'
|
||||
import { Plan } from '@typebot.io/prisma'
|
||||
|
||||
export const updateTypebot = authenticatedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
method: 'PATCH',
|
||||
path: '/typebots/{typebotId}',
|
||||
protect: true,
|
||||
summary: 'Update a typebot',
|
||||
tags: ['Typebot'],
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
typebotId: z.string(),
|
||||
typebot: typebotSchema
|
||||
.pick({
|
||||
name: true,
|
||||
icon: true,
|
||||
selectedThemeTemplateId: true,
|
||||
groups: true,
|
||||
theme: true,
|
||||
settings: true,
|
||||
folderId: true,
|
||||
variables: true,
|
||||
edges: true,
|
||||
isClosed: true,
|
||||
resultsTablePreferences: true,
|
||||
publicId: true,
|
||||
customDomain: true,
|
||||
})
|
||||
.partial(),
|
||||
updatedAt: z
|
||||
.date()
|
||||
.optional()
|
||||
.describe(
|
||||
'Used for checking if there is a newer version of the typebot in the database'
|
||||
),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
typebot: typebotSchema,
|
||||
})
|
||||
)
|
||||
.mutation(
|
||||
async ({ input: { typebotId, typebot, updatedAt }, ctx: { user } }) => {
|
||||
const existingTypebot = await prisma.typebot.findFirst({
|
||||
where: {
|
||||
id: typebotId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
customDomain: true,
|
||||
publicId: true,
|
||||
workspaceId: true,
|
||||
collaborators: {
|
||||
select: {
|
||||
userId: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
workspace: {
|
||||
select: {
|
||||
plan: true,
|
||||
},
|
||||
},
|
||||
updatedAt: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (
|
||||
!existingTypebot?.id ||
|
||||
(await isWriteTypebotForbidden(existingTypebot, user))
|
||||
)
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'Typebot not found',
|
||||
})
|
||||
|
||||
if (
|
||||
updatedAt &&
|
||||
updatedAt.getTime() > new Date(existingTypebot?.updatedAt).getTime()
|
||||
)
|
||||
throw new TRPCError({
|
||||
code: 'CONFLICT',
|
||||
message: 'Found newer version of the typebot in database',
|
||||
})
|
||||
|
||||
if (
|
||||
typebot.customDomain &&
|
||||
existingTypebot.customDomain !== typebot.customDomain &&
|
||||
(await isCustomDomainNotAvailable(typebot.customDomain))
|
||||
)
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Custom domain not available',
|
||||
})
|
||||
|
||||
if (typebot.publicId) {
|
||||
if (isCloudProdInstance && typebot.publicId.length < 4)
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Public id should be at least 4 characters long',
|
||||
})
|
||||
if (
|
||||
existingTypebot.publicId !== typebot.publicId &&
|
||||
(await isPublicIdNotAvailable(typebot.publicId))
|
||||
)
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Public id not available',
|
||||
})
|
||||
}
|
||||
|
||||
const newTypebot = await prisma.typebot.update({
|
||||
where: {
|
||||
id: existingTypebot.id,
|
||||
},
|
||||
data: {
|
||||
version: '5',
|
||||
name: typebot.name,
|
||||
icon: typebot.icon,
|
||||
selectedThemeTemplateId: typebot.selectedThemeTemplateId,
|
||||
groups: typebot.groups
|
||||
? await sanitizeGroups(existingTypebot.workspaceId)(typebot.groups)
|
||||
: [],
|
||||
theme: typebot.theme ? typebot.theme : defaultTheme,
|
||||
settings: typebot.settings
|
||||
? sanitizeSettings(typebot.settings, existingTypebot.workspace.plan)
|
||||
: defaultSettings({
|
||||
isBrandingEnabled: existingTypebot.workspace.plan !== Plan.FREE,
|
||||
}),
|
||||
folderId: typebot.folderId,
|
||||
variables: typebot.variables,
|
||||
edges: typebot.edges,
|
||||
resultsTablePreferences: typebot.resultsTablePreferences ?? undefined,
|
||||
publicId: typebot.publicId ?? undefined,
|
||||
customDomain: typebot.customDomain ?? undefined,
|
||||
isClosed: typebot.isClosed ?? false,
|
||||
},
|
||||
})
|
||||
|
||||
return { typebot: typebotSchema.parse(newTypebot) }
|
||||
}
|
||||
)
|
||||
@@ -1,36 +0,0 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { Prisma, User } from '@typebot.io/prisma'
|
||||
import { isReadTypebotForbidden } from './isReadTypebotForbidden'
|
||||
import { isWriteTypebotForbidden } from './isWriteTypebotForbidden'
|
||||
|
||||
type Props<T extends Prisma.TypebotSelect> = {
|
||||
typebotId: string
|
||||
user: Pick<User, 'id' | 'email'>
|
||||
accessLevel: 'read' | 'write'
|
||||
select?: T
|
||||
}
|
||||
|
||||
export const getTypebot = async <T extends Prisma.TypebotSelect>({
|
||||
typebotId,
|
||||
user,
|
||||
accessLevel,
|
||||
select,
|
||||
}: Props<T>) => {
|
||||
const typebot = await prisma.typebot.findFirst({
|
||||
where: {
|
||||
id: typebotId,
|
||||
},
|
||||
select: {
|
||||
...select,
|
||||
id: true,
|
||||
workspaceId: true,
|
||||
collaborators: { select: { userId: true, type: true } },
|
||||
},
|
||||
})
|
||||
if (!typebot) return null
|
||||
if (accessLevel === 'read' && (await isReadTypebotForbidden(typebot, user)))
|
||||
return null
|
||||
if (accessLevel === 'write' && (await isWriteTypebotForbidden(typebot, user)))
|
||||
return null
|
||||
return typebot
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Edge, Typebot, edgeSchema } from '@typebot.io/schemas'
|
||||
|
||||
export const parseInvalidTypebot = (typebot: Typebot): Typebot => ({
|
||||
...typebot,
|
||||
version: typebot.version as null | '3' | '4' | '5',
|
||||
edges: parseInvalidEdges(typebot.edges),
|
||||
})
|
||||
|
||||
const parseInvalidEdges = (edges: Edge[]) =>
|
||||
edges?.filter((edge) => edgeSchema.safeParse(edge).success)
|
||||
@@ -1,9 +0,0 @@
|
||||
import { omit } from '@typebot.io/lib'
|
||||
|
||||
export const removeTypebotOldProperties = (data: unknown) => {
|
||||
if (!data || typeof data !== 'object') return data
|
||||
if ('publishedTypebotId' in data) {
|
||||
return omit(data, 'publishedTypebotId')
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Typebot } from '@typebot.io/schemas'
|
||||
|
||||
export const roundGroupsCoordinate = (typebot: Typebot): Typebot => {
|
||||
const groups = typebot.groups.map((group) => {
|
||||
const { x, y } = group.graphCoordinates
|
||||
return {
|
||||
...group,
|
||||
graphCoordinates: { x: Number(x.toFixed(2)), y: Number(y.toFixed(2)) },
|
||||
}
|
||||
})
|
||||
return { ...typebot, groups }
|
||||
}
|
||||
152
apps/builder/src/features/typebot/helpers/sanitizers.ts
Normal file
152
apps/builder/src/features/typebot/helpers/sanitizers.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { Plan } from '@typebot.io/prisma'
|
||||
import {
|
||||
Block,
|
||||
InputBlockType,
|
||||
IntegrationBlockType,
|
||||
Typebot,
|
||||
Webhook,
|
||||
WebhookBlock,
|
||||
defaultWebhookAttributes,
|
||||
} from '@typebot.io/schemas'
|
||||
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums'
|
||||
|
||||
export const sanitizeSettings = (
|
||||
settings: Typebot['settings'],
|
||||
workspacePlan: Plan
|
||||
): Typebot['settings'] => ({
|
||||
...settings,
|
||||
general: {
|
||||
...settings.general,
|
||||
isBrandingEnabled:
|
||||
workspacePlan === Plan.FREE ? false : settings.general.isBrandingEnabled,
|
||||
},
|
||||
})
|
||||
|
||||
export const sanitizeGroups =
|
||||
(workspaceId: string) =>
|
||||
async (groups: Typebot['groups']): Promise<Typebot['groups']> =>
|
||||
Promise.all(
|
||||
groups.map(async (group) => ({
|
||||
...group,
|
||||
blocks: await Promise.all(group.blocks.map(sanitizeBlock(workspaceId))),
|
||||
}))
|
||||
)
|
||||
|
||||
const sanitizeBlock =
|
||||
(workspaceId: string) =>
|
||||
async (block: Block): Promise<Block> => {
|
||||
switch (block.type) {
|
||||
case InputBlockType.PAYMENT:
|
||||
return {
|
||||
...block,
|
||||
options: {
|
||||
...block.options,
|
||||
credentialsId: await sanitizeCredentialsId(workspaceId)(
|
||||
block.options.credentialsId
|
||||
),
|
||||
},
|
||||
}
|
||||
case IntegrationBlockType.WEBHOOK:
|
||||
return await sanitizeWebhookBlock(block)
|
||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||
return {
|
||||
...block,
|
||||
options: {
|
||||
...block.options,
|
||||
credentialsId: await sanitizeCredentialsId(workspaceId)(
|
||||
block.options.credentialsId
|
||||
),
|
||||
},
|
||||
}
|
||||
case IntegrationBlockType.OPEN_AI:
|
||||
return {
|
||||
...block,
|
||||
options: {
|
||||
...block.options,
|
||||
credentialsId: await sanitizeCredentialsId(workspaceId)(
|
||||
block.options.credentialsId
|
||||
),
|
||||
},
|
||||
}
|
||||
case IntegrationBlockType.EMAIL:
|
||||
return {
|
||||
...block,
|
||||
options: {
|
||||
...block.options,
|
||||
credentialsId:
|
||||
(await sanitizeCredentialsId(workspaceId)(
|
||||
block.options.credentialsId
|
||||
)) ?? 'default',
|
||||
},
|
||||
}
|
||||
default:
|
||||
return block
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizeWebhookBlock = async (
|
||||
block: WebhookBlock
|
||||
): Promise<WebhookBlock> => {
|
||||
if (!block.webhookId) return block
|
||||
const webhook = await prisma.webhook.findFirst({
|
||||
where: {
|
||||
id: block.webhookId,
|
||||
},
|
||||
})
|
||||
return {
|
||||
...block,
|
||||
webhookId: undefined,
|
||||
options: {
|
||||
...block.options,
|
||||
webhook: webhook
|
||||
? {
|
||||
id: webhook.id,
|
||||
url: webhook.url ?? undefined,
|
||||
method: (webhook.method as Webhook['method']) ?? HttpMethod.POST,
|
||||
headers: (webhook.headers as Webhook['headers']) ?? [],
|
||||
queryParams: (webhook.queryParams as Webhook['headers']) ?? [],
|
||||
body: webhook.body ?? undefined,
|
||||
}
|
||||
: {
|
||||
...defaultWebhookAttributes,
|
||||
id: block.webhookId ?? '',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizeCredentialsId =
|
||||
(workspaceId: string) =>
|
||||
async (credentialsId?: string): Promise<string | undefined> => {
|
||||
if (!credentialsId) return
|
||||
const credentials = await prisma.credentials.findFirst({
|
||||
where: {
|
||||
id: credentialsId,
|
||||
workspaceId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
})
|
||||
return credentials?.id
|
||||
}
|
||||
|
||||
export const isPublicIdNotAvailable = async (publicId: string) => {
|
||||
const typebotWithSameIdCount = await prisma.typebot.count({
|
||||
where: {
|
||||
publicId,
|
||||
},
|
||||
})
|
||||
return typebotWithSameIdCount > 0
|
||||
}
|
||||
|
||||
export const isCustomDomainNotAvailable = async (customDomain: string) => {
|
||||
const typebotWithSameDomainCount = await prisma.typebot.count({
|
||||
where: {
|
||||
customDomain,
|
||||
},
|
||||
})
|
||||
|
||||
return typebotWithSameDomainCount > 0
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
export const isCloudProdInstance =
|
||||
typeof window !== 'undefined' && window.location.hostname === 'app.typebot.io'
|
||||
(typeof window !== 'undefined' &&
|
||||
window.location.hostname === 'app.typebot.io') ||
|
||||
process.env.NEXTAUTH_URL === 'https://app.typebot.io'
|
||||
|
||||
@@ -10,6 +10,7 @@ import { typebotRouter } from '@/features/typebot/api/router'
|
||||
import { workspaceRouter } from '@/features/workspace/api/router'
|
||||
import { router } from '../../trpc'
|
||||
import { analyticsRouter } from '@/features/analytics/api/router'
|
||||
import { collaboratorsRouter } from '@/features/collaboration/api/router'
|
||||
|
||||
export const trpcRouter = router({
|
||||
getAppVersionProcedure,
|
||||
@@ -23,6 +24,7 @@ export const trpcRouter = router({
|
||||
billing: billingRouter,
|
||||
credentials: credentialsRouter,
|
||||
theme: themeRouter,
|
||||
collaborators: collaboratorsRouter,
|
||||
})
|
||||
|
||||
export type AppRouter = typeof trpcRouter
|
||||
|
||||
@@ -7,7 +7,8 @@ export const useAutoSave = <T>(
|
||||
item,
|
||||
debounceTimeout,
|
||||
}: {
|
||||
handler: () => Promise<void>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
handler: () => Promise<any>
|
||||
item?: T
|
||||
debounceTimeout: number
|
||||
},
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { fetcher } from '@/helpers/fetcher'
|
||||
import { PublicTypebot, Typebot, Webhook } from '@typebot.io/schemas'
|
||||
import useSWR from 'swr'
|
||||
import { env } from '@typebot.io/lib'
|
||||
|
||||
export const useTypebotQuery = ({
|
||||
typebotId,
|
||||
onError,
|
||||
}: {
|
||||
typebotId?: string
|
||||
onError?: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<
|
||||
{
|
||||
typebot: Typebot
|
||||
webhooks: Webhook[]
|
||||
publishedTypebot?: PublicTypebot
|
||||
isReadOnly?: boolean
|
||||
},
|
||||
Error
|
||||
>(typebotId ? `/api/typebots/${typebotId}` : null, fetcher, {
|
||||
dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
|
||||
})
|
||||
if (error && onError) onError(error)
|
||||
return {
|
||||
typebot: data?.typebot,
|
||||
webhooks: data?.webhooks,
|
||||
publishedTypebot: data?.publishedTypebot,
|
||||
isReadOnly: data?.isReadOnly,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { httpBatchLink, loggerLink } from '@trpc/client'
|
||||
import { createTRPCProxyClient, httpBatchLink, loggerLink } from '@trpc/client'
|
||||
import { createTRPCNext } from '@trpc/next'
|
||||
import type { AppRouter } from '../helpers/server/routers/v1/trpcRouter'
|
||||
import superjson from 'superjson'
|
||||
@@ -26,6 +26,15 @@ export const trpc = createTRPCNext<AppRouter>({
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
export const trpcVanilla = createTRPCProxyClient<AppRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: `${getBaseUrl()}/api/trpc`,
|
||||
}),
|
||||
],
|
||||
transformer: superjson,
|
||||
})
|
||||
|
||||
export const defaultQueryOptions = {
|
||||
refetchOnMount: env('E2E_TEST') === 'true',
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { omit } from '@typebot.io/lib'
|
||||
import { sendTelemetryEvents } from '@typebot.io/lib/telemetry/sendTelemetryEvent'
|
||||
|
||||
// TODO: Delete
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const user = await getAuthenticatedUser(req, res)
|
||||
if (!user) return notAuthenticated(res)
|
||||
|
||||
@@ -3,15 +3,14 @@ import prisma from '@/lib/prisma'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api'
|
||||
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
|
||||
import { Typebot } from '@typebot.io/schemas'
|
||||
import { Group, Typebot } from '@typebot.io/schemas'
|
||||
import { omit } from '@typebot.io/lib'
|
||||
import { getTypebot } from '@/features/typebot/helpers/getTypebot'
|
||||
import { isReadTypebotForbidden } from '@/features/typebot/helpers/isReadTypebotForbidden'
|
||||
import { removeTypebotOldProperties } from '@/features/typebot/helpers/removeTypebotOldProperties'
|
||||
import { roundGroupsCoordinate } from '@/features/typebot/helpers/roundGroupsCoordinate'
|
||||
import { archiveResults } from '@typebot.io/lib/api/helpers/archiveResults'
|
||||
import { migrateTypebotFromV3ToV4 } from '@typebot.io/lib/migrations/migrateTypebotFromV3ToV4'
|
||||
import { isWriteTypebotForbidden } from '@/features/typebot/helpers/isWriteTypebotForbidden'
|
||||
|
||||
// TODO: delete
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const user = await getAuthenticatedUser(req, res)
|
||||
if (!user) return notAuthenticated(res)
|
||||
@@ -46,17 +45,28 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
}
|
||||
|
||||
if (req.method === 'DELETE') {
|
||||
const typebot = (await getTypebot({
|
||||
accessLevel: 'write',
|
||||
user,
|
||||
typebotId,
|
||||
select: {
|
||||
groups: true,
|
||||
const typebot = await prisma.typebot.findUnique({
|
||||
where: {
|
||||
id: typebotId,
|
||||
},
|
||||
})) as Pick<Typebot, 'groups'> | null
|
||||
if (!typebot) return res.status(404).send({ typebot: null })
|
||||
select: {
|
||||
id: true,
|
||||
workspaceId: true,
|
||||
groups: true,
|
||||
collaborators: {
|
||||
select: {
|
||||
userId: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!typebot || (await isWriteTypebotForbidden(typebot, user)))
|
||||
return res.status(404).send({ typebot: null })
|
||||
const { success } = await archiveResults(prisma)({
|
||||
typebot,
|
||||
typebot: {
|
||||
groups: typebot.groups as Group[],
|
||||
},
|
||||
resultsFilter: { typebotId },
|
||||
})
|
||||
if (!success) return res.status(500).send({ success: false, error: '' })
|
||||
@@ -75,15 +85,25 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||
) as Typebot
|
||||
|
||||
const typebot = await getTypebot({
|
||||
accessLevel: 'write',
|
||||
typebotId,
|
||||
user,
|
||||
const typebot = await prisma.typebot.findUnique({
|
||||
where: {
|
||||
id: typebotId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
workspaceId: true,
|
||||
groups: true,
|
||||
updatedAt: true,
|
||||
collaborators: {
|
||||
select: {
|
||||
userId: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!typebot) return res.status(404).send({ message: 'Typebot not found' })
|
||||
if (!typebot || (await isWriteTypebotForbidden(typebot, user)))
|
||||
return res.status(404).send({ message: 'Typebot not found' })
|
||||
|
||||
if (
|
||||
(typebot.updatedAt as Date).getTime() > new Date(data.updatedAt).getTime()
|
||||
@@ -124,12 +144,24 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
}
|
||||
|
||||
if (req.method === 'PATCH') {
|
||||
const typebot = await getTypebot({
|
||||
accessLevel: 'write',
|
||||
typebotId,
|
||||
user,
|
||||
const typebot = await prisma.typebot.findUnique({
|
||||
where: {
|
||||
id: typebotId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
workspaceId: true,
|
||||
groups: true,
|
||||
collaborators: {
|
||||
select: {
|
||||
userId: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!typebot) return res.status(404).send({ message: 'Typebot not found' })
|
||||
if (!typebot || (await isWriteTypebotForbidden(typebot, user)))
|
||||
return res.status(404).send({ message: 'Typebot not found' })
|
||||
const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||
const updatedTypebot = await prisma.typebot.update({
|
||||
where: { id: typebotId },
|
||||
@@ -142,10 +174,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
|
||||
const migrateTypebot = async (typebot: Typebot): Promise<Typebot> => {
|
||||
if (typebot.version === '4') return typebot
|
||||
const updatedTypebot = roundGroupsCoordinate(
|
||||
removeTypebotOldProperties(typebot) as Typebot
|
||||
)
|
||||
return migrateTypebotFromV3ToV4(prisma)(updatedTypebot)
|
||||
return migrateTypebotFromV3ToV4(prisma)(typebot)
|
||||
}
|
||||
|
||||
export default handler
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { defaultWebhookAttributes, Webhook } from '@typebot.io/schemas'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
|
||||
import {
|
||||
methodNotAllowed,
|
||||
notAuthenticated,
|
||||
notFound,
|
||||
} from '@typebot.io/lib/api'
|
||||
import { getTypebot } from '@/features/typebot/helpers/getTypebot'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const user = await getAuthenticatedUser(req, res)
|
||||
if (!user) return notAuthenticated(res)
|
||||
if (req.method === 'POST') {
|
||||
const typebotId = req.query.typebotId as string
|
||||
const data = req.body.data as Partial<Webhook>
|
||||
const typebot = await getTypebot({
|
||||
accessLevel: 'write',
|
||||
user,
|
||||
typebotId,
|
||||
})
|
||||
if (!typebot) return notFound(res)
|
||||
const webhook = await prisma.webhook.create({
|
||||
data: { ...defaultWebhookAttributes, ...data, typebotId },
|
||||
})
|
||||
return res.send({ webhook })
|
||||
}
|
||||
methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default handler
|
||||
@@ -1,55 +0,0 @@
|
||||
import prisma from '@/lib/prisma'
|
||||
import { Webhook } from '@typebot.io/schemas'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
|
||||
import {
|
||||
badRequest,
|
||||
forbidden,
|
||||
methodNotAllowed,
|
||||
notAuthenticated,
|
||||
notFound,
|
||||
} from '@typebot.io/lib/api'
|
||||
import { getTypebot } from '@/features/typebot/helpers/getTypebot'
|
||||
import { omit } from '@typebot.io/lib'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const user = await getAuthenticatedUser(req, res)
|
||||
const typebotId = req.query.typebotId as string
|
||||
const webhookId = req.query.webhookId as string
|
||||
if (!user) return notAuthenticated(res)
|
||||
if (req.method === 'GET') {
|
||||
const typebot = getTypebot({
|
||||
accessLevel: 'read',
|
||||
typebotId,
|
||||
user,
|
||||
})
|
||||
if (!typebot) return notFound(res)
|
||||
const webhook = await prisma.webhook.findFirst({
|
||||
where: {
|
||||
id: webhookId,
|
||||
typebotId,
|
||||
},
|
||||
})
|
||||
return res.send({ webhook })
|
||||
}
|
||||
if (req.method === 'PATCH') {
|
||||
const data = req.body.data as Partial<Webhook>
|
||||
if (!('typebotId' in data)) return badRequest(res)
|
||||
const typebot = await getTypebot({
|
||||
accessLevel: 'write',
|
||||
typebotId,
|
||||
user,
|
||||
})
|
||||
if (!typebot) return forbidden(res)
|
||||
const webhook = await prisma.webhook.update({
|
||||
where: {
|
||||
id: webhookId,
|
||||
},
|
||||
data: omit(data, 'id', 'typebotId'),
|
||||
})
|
||||
return res.send({ webhook })
|
||||
}
|
||||
methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default handler
|
||||
File diff suppressed because it is too large
Load Diff
@@ -584,8 +584,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"labels",
|
||||
"retryMessageContent"
|
||||
"labels"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -4342,8 +4341,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"labels",
|
||||
"retryMessageContent"
|
||||
"labels"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
SetVariableBlock,
|
||||
WebhookBlock,
|
||||
defaultPaymentInputOptions,
|
||||
invalidEmailDefaultRetryMessage,
|
||||
} from '@typebot.io/schemas'
|
||||
import { isInputBlock, byId } from '@typebot.io/lib'
|
||||
import { executeGroup } from './executeGroup'
|
||||
@@ -207,6 +208,8 @@ const parseRetryMessage = (
|
||||
|
||||
const parseDefaultRetryMessage = (block: InputBlock): string => {
|
||||
switch (block.type) {
|
||||
case InputBlockType.EMAIL:
|
||||
return invalidEmailDefaultRetryMessage
|
||||
case InputBlockType.PAYMENT:
|
||||
return defaultPaymentInputOptions.retryMessageContent as string
|
||||
default:
|
||||
|
||||
@@ -12,15 +12,17 @@ import {
|
||||
} from '@typebot.io/lib/playwright/databaseActions'
|
||||
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
|
||||
|
||||
const settings = defaultSettings({ isBrandingEnabled: true })
|
||||
|
||||
test('Result should be overwritten on page refresh', async ({ page }) => {
|
||||
const typebotId = createId()
|
||||
await createTypebots([
|
||||
{
|
||||
id: typebotId,
|
||||
settings: {
|
||||
...defaultSettings,
|
||||
...settings,
|
||||
general: {
|
||||
...defaultSettings.general,
|
||||
...settings.general,
|
||||
rememberUser: {
|
||||
isEnabled: true,
|
||||
storage: 'session',
|
||||
@@ -96,8 +98,8 @@ test('Hide query params', async ({ page }) => {
|
||||
await updateTypebot({
|
||||
id: typebotId,
|
||||
settings: {
|
||||
...defaultSettings,
|
||||
general: { ...defaultSettings.general, isHideQueryParamsEnabled: false },
|
||||
...settings,
|
||||
general: { ...settings.general, isHideQueryParamsEnabled: false },
|
||||
},
|
||||
})
|
||||
await page.goto(`/${typebotId}-public?Name=John`)
|
||||
@@ -136,7 +138,7 @@ test('Should correctly parse metadata', async ({ page }) => {
|
||||
{
|
||||
id: typebotId,
|
||||
settings: {
|
||||
...defaultSettings,
|
||||
...settings,
|
||||
metadata: customMetadata,
|
||||
},
|
||||
...parseDefaultGroupWithBlock({
|
||||
|
||||
@@ -21,7 +21,7 @@ export const parseTestTypebot = (
|
||||
folderId: null,
|
||||
name: 'My typebot',
|
||||
theme: defaultTheme,
|
||||
settings: defaultSettings,
|
||||
settings: defaultSettings({ isBrandingEnabled: true }),
|
||||
publicId: null,
|
||||
updatedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
|
||||
@@ -8,7 +8,7 @@ export const emailInputOptionsSchema = optionBaseSchema
|
||||
.merge(textInputOptionsBaseSchema)
|
||||
.merge(
|
||||
z.object({
|
||||
retryMessageContent: z.string(),
|
||||
retryMessageContent: z.string().optional(),
|
||||
})
|
||||
)
|
||||
|
||||
@@ -19,13 +19,14 @@ export const emailInputSchema = blockBaseSchema.merge(
|
||||
})
|
||||
)
|
||||
|
||||
export const invalidEmailDefaultRetryMessage =
|
||||
"This email doesn't seem to be valid. Can you type it again?"
|
||||
|
||||
export const defaultEmailInputOptions: EmailInputOptions = {
|
||||
labels: {
|
||||
button: defaultButtonLabel,
|
||||
placeholder: 'Type your email...',
|
||||
},
|
||||
retryMessageContent:
|
||||
"This email doesn't seem to be valid. Can you type it again?",
|
||||
}
|
||||
|
||||
export type EmailInputBlock = z.infer<typeof emailInputSchema>
|
||||
|
||||
10
packages/schemas/features/collaborators.ts
Normal file
10
packages/schemas/features/collaborators.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { CollaborationType, CollaboratorsOnTypebots } from '@typebot.io/prisma'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const collaboratorSchema = z.object({
|
||||
type: z.nativeEnum(CollaborationType),
|
||||
userId: z.string(),
|
||||
typebotId: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
}) satisfies z.ZodType<CollaboratorsOnTypebots>
|
||||
@@ -11,7 +11,7 @@ import { z } from 'zod'
|
||||
|
||||
export const publicTypebotSchema = z.object({
|
||||
id: z.string(),
|
||||
version: z.enum(['3', '4']).nullable(),
|
||||
version: z.enum(['3', '4', '5']).nullable(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
typebotId: z.string(),
|
||||
|
||||
@@ -37,9 +37,13 @@ export const settingsSchema = z.object({
|
||||
metadata: metadataSchema,
|
||||
})
|
||||
|
||||
export const defaultSettings: Settings = {
|
||||
export const defaultSettings = ({
|
||||
isBrandingEnabled,
|
||||
}: {
|
||||
isBrandingEnabled: boolean
|
||||
}): Settings => ({
|
||||
general: {
|
||||
isBrandingEnabled: true,
|
||||
isBrandingEnabled,
|
||||
rememberUser: {
|
||||
isEnabled: false,
|
||||
},
|
||||
@@ -51,7 +55,7 @@ export const defaultSettings: Settings = {
|
||||
description:
|
||||
'Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form.',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export type Settings = z.infer<typeof settingsSchema>
|
||||
export type GeneralSettings = z.infer<typeof generalSettings>
|
||||
|
||||
@@ -38,8 +38,11 @@ const resultsTablePreferencesSchema = z.object({
|
||||
columnsWidth: z.record(z.string(), z.number()),
|
||||
})
|
||||
|
||||
const isPathNameCompatible = (str: string) =>
|
||||
/^([a-z0-9]+-[a-z0-9]*)*$/.test(str) || /^[a-z0-9]*$/.test(str)
|
||||
|
||||
export const typebotSchema = z.object({
|
||||
version: z.enum(['3', '4']).nullable(),
|
||||
version: z.enum(['3', '4', '5']).nullable(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
groups: z.array(groupSchema),
|
||||
@@ -52,8 +55,8 @@ export const typebotSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
icon: z.string().nullable(),
|
||||
folderId: z.string().nullable(),
|
||||
publicId: z.string().nullable(),
|
||||
customDomain: z.string().nullable(),
|
||||
publicId: z.string().refine(isPathNameCompatible).nullable(),
|
||||
customDomain: z.string().refine(isPathNameCompatible).nullable(),
|
||||
workspaceId: z.string(),
|
||||
resultsTablePreferences: resultsTablePreferencesSchema.nullable(),
|
||||
isArchived: z.boolean(),
|
||||
|
||||
Reference in New Issue
Block a user