🧑💻 (folders) Add folder trpc endpoints (#1218)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced folder management capabilities including creation, deletion, update, listing, and retrieval within workspaces. - Added telemetry tracking for client events, Typebot publish events, and analytics page views. - Enhanced settings to track client events under specific conditions. - Implemented server-side logic for analytics tracking with PostHog integration. - Added API documentation for folder operations (create, delete, get, list, update). - **Refactor** - Updated `onConfirm` function's return type in `ConfirmModal`. - Simplified folder creation process in tests. - Refactored logic for handling file upload blocks and parsing publish events in Typebot publishing. - Migrated handler functions to TRPC endpoints for folder operations. - **Documentation** - Introduced documentation for new folder and telemetry functionalities. - **Chores** - Added new schemas for folders and telemetry events, including event tracking and folder structure. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@ -12,7 +12,7 @@ import { useTranslate } from '@tolgee/react'
|
|||||||
|
|
||||||
type ConfirmDeleteModalProps = {
|
type ConfirmDeleteModalProps = {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onConfirm: () => Promise<unknown>
|
onConfirm: () => Promise<unknown> | unknown
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
message: JSX.Element
|
message: JSX.Element
|
||||||
title?: string
|
title?: string
|
||||||
|
@ -11,10 +11,7 @@ test('folders navigation should work', async ({ page }) => {
|
|||||||
await createFolderButton.click()
|
await createFolderButton.click()
|
||||||
await page.click('text="New folder"')
|
await page.click('text="New folder"')
|
||||||
await page.fill('input[value="New folder"]', 'My folder #1')
|
await page.fill('input[value="New folder"]', 'My folder #1')
|
||||||
await Promise.all([
|
await page.press('input[value="My folder #1"]', 'Enter')
|
||||||
page.waitForResponse((resp) => resp.request().method() === 'PATCH'),
|
|
||||||
page.press('input[value="My folder #1"]', 'Enter'),
|
|
||||||
])
|
|
||||||
await page.click('li:has-text("My folder #1")')
|
await page.click('li:has-text("My folder #1")')
|
||||||
await expect(page.locator('h1:has-text("My folder #1")')).toBeVisible()
|
await expect(page.locator('h1:has-text("My folder #1")')).toBeVisible()
|
||||||
await createFolderButton.click()
|
await createFolderButton.click()
|
||||||
|
76
apps/builder/src/features/folders/api/createFolder.ts
Normal file
76
apps/builder/src/features/folders/api/createFolder.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import prisma from '@typebot.io/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { DashboardFolder, Plan, WorkspaceRole } from '@typebot.io/prisma'
|
||||||
|
import { folderSchema } from '@typebot.io/schemas'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { getUserRoleInWorkspace } from '@/features/workspace/helpers/getUserRoleInWorkspace'
|
||||||
|
import { trackEvents } from '@typebot.io/lib/telemetry/trackEvents'
|
||||||
|
|
||||||
|
export const createFolder = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'POST',
|
||||||
|
path: '/v1/folders',
|
||||||
|
protect: true,
|
||||||
|
summary: 'Create a folder',
|
||||||
|
tags: ['Folder'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
folderName: z.string().default('New folder'),
|
||||||
|
parentFolderId: z.string().optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
folder: folderSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(
|
||||||
|
async ({
|
||||||
|
input: { folderName, parentFolderId, 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 (workspace.plan === Plan.FREE)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'FORBIDDEN',
|
||||||
|
message: 'You need to upgrade to a paid plan to create folders',
|
||||||
|
})
|
||||||
|
|
||||||
|
const newFolder = await prisma.dashboardFolder.create({
|
||||||
|
data: {
|
||||||
|
workspaceId,
|
||||||
|
name: folderName,
|
||||||
|
parentFolderId,
|
||||||
|
} satisfies Partial<DashboardFolder>,
|
||||||
|
})
|
||||||
|
|
||||||
|
await trackEvents([
|
||||||
|
{
|
||||||
|
name: 'Folder created',
|
||||||
|
userId: user.id,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
return { folder: folderSchema.parse(newFolder) }
|
||||||
|
}
|
||||||
|
)
|
53
apps/builder/src/features/folders/api/deleteFolder.ts
Normal file
53
apps/builder/src/features/folders/api/deleteFolder.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import prisma from '@typebot.io/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { WorkspaceRole } from '@typebot.io/prisma'
|
||||||
|
import { folderSchema } from '@typebot.io/schemas'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { getUserRoleInWorkspace } from '@/features/workspace/helpers/getUserRoleInWorkspace'
|
||||||
|
|
||||||
|
export const deleteFolder = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'DELETE',
|
||||||
|
path: '/v1/folders/{folderId}',
|
||||||
|
protect: true,
|
||||||
|
summary: 'Delete a folder',
|
||||||
|
tags: ['Folder'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
folderId: z.string(),
|
||||||
|
workspaceId: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
folder: folderSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ input: { folderId, 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',
|
||||||
|
})
|
||||||
|
|
||||||
|
const folder = await prisma.dashboardFolder.delete({
|
||||||
|
where: {
|
||||||
|
id: folderId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return { folder }
|
||||||
|
})
|
59
apps/builder/src/features/folders/api/getFolder.ts
Normal file
59
apps/builder/src/features/folders/api/getFolder.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import prisma from '@typebot.io/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { WorkspaceRole } from '@typebot.io/prisma'
|
||||||
|
import { folderSchema } from '@typebot.io/schemas'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { getUserRoleInWorkspace } from '@/features/workspace/helpers/getUserRoleInWorkspace'
|
||||||
|
|
||||||
|
export const getFolder = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'GET',
|
||||||
|
path: '/v1/folders/{folderId}',
|
||||||
|
protect: true,
|
||||||
|
summary: 'Get folder',
|
||||||
|
tags: ['Folder'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
folderId: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
folder: folderSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input: { workspaceId, folderId }, 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',
|
||||||
|
})
|
||||||
|
|
||||||
|
const folder = await prisma.dashboardFolder.findUnique({
|
||||||
|
where: {
|
||||||
|
id: folderId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!folder)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'Folder not found',
|
||||||
|
})
|
||||||
|
|
||||||
|
return { folder }
|
||||||
|
})
|
57
apps/builder/src/features/folders/api/listFolders.ts
Normal file
57
apps/builder/src/features/folders/api/listFolders.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import prisma from '@typebot.io/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { WorkspaceRole } from '@typebot.io/prisma'
|
||||||
|
import { folderSchema } from '@typebot.io/schemas'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { getUserRoleInWorkspace } from '@/features/workspace/helpers/getUserRoleInWorkspace'
|
||||||
|
|
||||||
|
export const listFolders = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'GET',
|
||||||
|
path: '/v1/folders',
|
||||||
|
protect: true,
|
||||||
|
summary: 'List folders',
|
||||||
|
tags: ['Folder'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
parentFolderId: z.string().optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
folders: z.array(folderSchema),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input: { workspaceId, parentFolderId }, 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',
|
||||||
|
})
|
||||||
|
|
||||||
|
const folders = await prisma.dashboardFolder.findMany({
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
parentFolderId,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return { folders }
|
||||||
|
})
|
14
apps/builder/src/features/folders/api/router.ts
Normal file
14
apps/builder/src/features/folders/api/router.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { router } from '@/helpers/server/trpc'
|
||||||
|
import { createFolder } from './createFolder'
|
||||||
|
import { updateFolder } from './updateFolder'
|
||||||
|
import { deleteFolder } from './deleteFolder'
|
||||||
|
import { listFolders } from './listFolders'
|
||||||
|
import { getFolder } from './getFolder'
|
||||||
|
|
||||||
|
export const folderRouter = router({
|
||||||
|
getFolder,
|
||||||
|
createFolder,
|
||||||
|
updateFolder,
|
||||||
|
deleteFolder,
|
||||||
|
listFolders,
|
||||||
|
})
|
71
apps/builder/src/features/folders/api/updateFolder.ts
Normal file
71
apps/builder/src/features/folders/api/updateFolder.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import prisma from '@typebot.io/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { Plan, WorkspaceRole } from '@typebot.io/prisma'
|
||||||
|
import { folderSchema } from '@typebot.io/schemas'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { getUserRoleInWorkspace } from '@/features/workspace/helpers/getUserRoleInWorkspace'
|
||||||
|
|
||||||
|
export const updateFolder = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'PATCH',
|
||||||
|
path: '/v1/folders/{folderId}',
|
||||||
|
protect: true,
|
||||||
|
summary: 'Update a folder',
|
||||||
|
tags: ['Folder'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
folderId: z.string(),
|
||||||
|
workspaceId: z.string(),
|
||||||
|
folder: folderSchema
|
||||||
|
.pick({
|
||||||
|
name: true,
|
||||||
|
parentFolderId: true,
|
||||||
|
})
|
||||||
|
.partial(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
folder: folderSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(
|
||||||
|
async ({ input: { folder, folderId, 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 (workspace.plan === Plan.FREE)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'FORBIDDEN',
|
||||||
|
message: 'You need to upgrade to a paid plan to update folders',
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedFolder = await prisma.dashboardFolder.update({
|
||||||
|
where: {
|
||||||
|
id: folderId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
name: folder.name,
|
||||||
|
parentFolderId: folder.parentFolderId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return { folder: folderSchema.parse(updatedFolder) }
|
||||||
|
}
|
||||||
|
)
|
@ -22,10 +22,9 @@ import { ConfirmModal } from '@/components/ConfirmModal'
|
|||||||
import { useTypebotDnd } from '../TypebotDndProvider'
|
import { useTypebotDnd } from '../TypebotDndProvider'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { deleteFolderQuery } from '../queries/deleteFolderQuery'
|
|
||||||
import { useToast } from '@/hooks/useToast'
|
import { useToast } from '@/hooks/useToast'
|
||||||
import { updateFolderQuery } from '../queries/updateFolderQuery'
|
|
||||||
import { T, useTranslate } from '@tolgee/react'
|
import { T, useTranslate } from '@tolgee/react'
|
||||||
|
import { trpc } from '@/lib/trpc'
|
||||||
|
|
||||||
export const FolderButton = ({
|
export const FolderButton = ({
|
||||||
folder,
|
folder,
|
||||||
@ -34,7 +33,7 @@ export const FolderButton = ({
|
|||||||
}: {
|
}: {
|
||||||
folder: DashboardFolder
|
folder: DashboardFolder
|
||||||
onFolderDeleted: () => void
|
onFolderDeleted: () => void
|
||||||
onFolderRenamed: (newName: string) => void
|
onFolderRenamed: () => void
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslate()
|
const { t } = useTranslate()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -47,21 +46,29 @@ export const FolderButton = ({
|
|||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
|
||||||
const onDeleteClick = async () => {
|
const { mutate: deleteFolder } = trpc.folders.deleteFolder.useMutation({
|
||||||
const { error } = await deleteFolderQuery(folder.id)
|
onError: (error) => {
|
||||||
return error
|
showToast({ description: error.message })
|
||||||
? showToast({
|
},
|
||||||
description: error.message,
|
onSuccess: onFolderDeleted,
|
||||||
})
|
})
|
||||||
: onFolderDeleted()
|
|
||||||
}
|
const { mutate: updateFolder } = trpc.folders.updateFolder.useMutation({
|
||||||
|
onError: (error) => {
|
||||||
|
showToast({ description: error.message })
|
||||||
|
},
|
||||||
|
onSuccess: onFolderRenamed,
|
||||||
|
})
|
||||||
|
|
||||||
const onRenameSubmit = async (newName: string) => {
|
const onRenameSubmit = async (newName: string) => {
|
||||||
if (newName === '' || newName === folder.name) return
|
if (newName === '' || newName === folder.name) return
|
||||||
const { error } = await updateFolderQuery(folder.id, { name: newName })
|
updateFolder({
|
||||||
return error
|
workspaceId: folder.workspaceId,
|
||||||
? showToast({ title: t('errorMessage'), description: error.message })
|
folderId: folder.id,
|
||||||
: onFolderRenamed(newName)
|
folder: {
|
||||||
|
name: newName,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
@ -148,7 +155,12 @@ export const FolderButton = ({
|
|||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
title={`Delete ${folder.name}?`}
|
title={`Delete ${folder.name}?`}
|
||||||
onConfirm={onDeleteClick}
|
onConfirm={() =>
|
||||||
|
deleteFolder({
|
||||||
|
workspaceId: folder.workspaceId,
|
||||||
|
folderId: folder.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
confirmButtonColor="red"
|
confirmButtonColor="red"
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -14,14 +14,11 @@ import React, { useState } from 'react'
|
|||||||
import { BackButton } from './BackButton'
|
import { BackButton } from './BackButton'
|
||||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||||
import { useToast } from '@/hooks/useToast'
|
import { useToast } from '@/hooks/useToast'
|
||||||
import { useFolders } from '../hooks/useFolders'
|
|
||||||
import { createFolderQuery } from '../queries/createFolderQuery'
|
|
||||||
import { CreateBotButton } from './CreateBotButton'
|
import { CreateBotButton } from './CreateBotButton'
|
||||||
import { CreateFolderButton } from './CreateFolderButton'
|
import { CreateFolderButton } from './CreateFolderButton'
|
||||||
import { ButtonSkeleton, FolderButton } from './FolderButton'
|
import { ButtonSkeleton, FolderButton } from './FolderButton'
|
||||||
import { TypebotButton } from './TypebotButton'
|
import { TypebotButton } from './TypebotButton'
|
||||||
import { TypebotCardOverlay } from './TypebotButtonOverlay'
|
import { TypebotCardOverlay } from './TypebotButtonOverlay'
|
||||||
import { useTranslate } from '@tolgee/react'
|
|
||||||
import { useTypebots } from '@/features/dashboard/hooks/useTypebots'
|
import { useTypebots } from '@/features/dashboard/hooks/useTypebots'
|
||||||
import { TypebotInDashboard } from '@/features/dashboard/types'
|
import { TypebotInDashboard } from '@/features/dashboard/types'
|
||||||
import { trpc } from '@/lib/trpc'
|
import { trpc } from '@/lib/trpc'
|
||||||
@ -31,7 +28,6 @@ type Props = { folder: DashboardFolder | null }
|
|||||||
const dragDistanceTolerance = 20
|
const dragDistanceTolerance = 20
|
||||||
|
|
||||||
export const FolderContent = ({ folder }: Props) => {
|
export const FolderContent = ({ folder }: Props) => {
|
||||||
const { t } = useTranslate()
|
|
||||||
const { workspace, currentRole } = useWorkspace()
|
const { workspace, currentRole } = useWorkspace()
|
||||||
const [isCreatingFolder, setIsCreatingFolder] = useState(false)
|
const [isCreatingFolder, setIsCreatingFolder] = useState(false)
|
||||||
const {
|
const {
|
||||||
@ -52,16 +48,30 @@ export const FolderContent = ({ folder }: Props) => {
|
|||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
folders,
|
data: { folders } = {},
|
||||||
isLoading: isFolderLoading,
|
isLoading: isFolderLoading,
|
||||||
mutate: mutateFolders,
|
refetch: refetchFolders,
|
||||||
} = useFolders({
|
} = trpc.folders.listFolders.useQuery(
|
||||||
workspaceId: workspace?.id,
|
{
|
||||||
parentId: folder?.id,
|
workspaceId: workspace?.id as string,
|
||||||
|
parentFolderId: folder?.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!workspace,
|
||||||
|
onError: (error) => {
|
||||||
|
showToast({
|
||||||
|
description: error.message,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { mutate: createFolder } = trpc.folders.createFolder.useMutation({
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
showToast({
|
showToast({ description: error.message })
|
||||||
description: error.message,
|
},
|
||||||
})
|
onSuccess: () => {
|
||||||
|
refetchFolders()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -98,36 +108,14 @@ export const FolderContent = ({ folder }: Props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreateFolder = async () => {
|
const handleCreateFolder = () => {
|
||||||
if (!folders || !workspace) return
|
if (!folders || !workspace) return
|
||||||
setIsCreatingFolder(true)
|
setIsCreatingFolder(true)
|
||||||
const { error, data: newFolder } = await createFolderQuery(workspace.id, {
|
createFolder({
|
||||||
parentFolderId: folder?.id ?? null,
|
workspaceId: workspace.id,
|
||||||
|
parentFolderId: folder?.id,
|
||||||
})
|
})
|
||||||
setIsCreatingFolder(false)
|
setIsCreatingFolder(false)
|
||||||
if (error)
|
|
||||||
return showToast({
|
|
||||||
title: t('errorMessage'),
|
|
||||||
description: error.message,
|
|
||||||
})
|
|
||||||
if (newFolder) mutateFolders({ folders: [...folders, newFolder] })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTypebotUpdated = () => {
|
|
||||||
if (!typebots) return
|
|
||||||
refetchTypebots()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFolderDeleted = (deletedId: string) => {
|
|
||||||
if (!folders) return
|
|
||||||
mutateFolders({ folders: folders.filter((f) => f.id !== deletedId) })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFolderRenamed = (folderId: string, name: string) => {
|
|
||||||
if (!folders) return
|
|
||||||
mutateFolders({
|
|
||||||
folders: folders.map((f) => (f.id === folderId ? { ...f, name } : f)),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseUp = async () => {
|
const handleMouseUp = async () => {
|
||||||
@ -169,7 +157,7 @@ export const FolderContent = ({ folder }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex w="full" flex="1" justify="center">
|
<Flex w="full" flex="1" justify="center">
|
||||||
<Stack w="1000px" spacing={6}>
|
<Stack w="1000px" spacing={6} pt="4">
|
||||||
<Skeleton isLoaded={folder?.name !== undefined}>
|
<Skeleton isLoaded={folder?.name !== undefined}>
|
||||||
<Heading as="h1">{folder?.name}</Heading>
|
<Heading as="h1">{folder?.name}</Heading>
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
@ -197,10 +185,8 @@ export const FolderContent = ({ folder }: Props) => {
|
|||||||
<FolderButton
|
<FolderButton
|
||||||
key={folder.id.toString()}
|
key={folder.id.toString()}
|
||||||
folder={folder}
|
folder={folder}
|
||||||
onFolderDeleted={() => handleFolderDeleted(folder.id)}
|
onFolderDeleted={refetchFolders}
|
||||||
onFolderRenamed={(newName: string) =>
|
onFolderRenamed={() => refetchFolders()}
|
||||||
handleFolderRenamed(folder.id, newName)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{isTypebotLoading && <ButtonSkeleton />}
|
{isTypebotLoading && <ButtonSkeleton />}
|
||||||
@ -209,7 +195,7 @@ export const FolderContent = ({ folder }: Props) => {
|
|||||||
<TypebotButton
|
<TypebotButton
|
||||||
key={typebot.id.toString()}
|
key={typebot.id.toString()}
|
||||||
typebot={typebot}
|
typebot={typebot}
|
||||||
onTypebotUpdated={handleTypebotUpdated}
|
onTypebotUpdated={refetchTypebots}
|
||||||
onMouseDown={handleMouseDown(typebot)}
|
onMouseDown={handleMouseDown(typebot)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -4,24 +4,32 @@ import { useToast } from '@/hooks/useToast'
|
|||||||
import { useTranslate } from '@tolgee/react'
|
import { useTranslate } from '@tolgee/react'
|
||||||
import { Stack, Flex, Spinner } from '@chakra-ui/react'
|
import { Stack, Flex, Spinner } from '@chakra-ui/react'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useFolder } from '../hooks/useFolder'
|
|
||||||
import { TypebotDndProvider } from '../TypebotDndProvider'
|
import { TypebotDndProvider } from '../TypebotDndProvider'
|
||||||
import { FolderContent } from './FolderContent'
|
import { FolderContent } from './FolderContent'
|
||||||
|
import { trpc } from '@/lib/trpc'
|
||||||
|
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||||
|
|
||||||
export const FolderPage = () => {
|
export const FolderPage = () => {
|
||||||
const { t } = useTranslate()
|
const { t } = useTranslate()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { workspace } = useWorkspace()
|
||||||
|
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
|
||||||
const { folder } = useFolder({
|
const { data: { folder } = {} } = trpc.folders.getFolder.useQuery(
|
||||||
folderId: router.query.id?.toString(),
|
{
|
||||||
onError: (error) => {
|
folderId: router.query.id as string,
|
||||||
showToast({
|
workspaceId: workspace?.id as string,
|
||||||
description: error.message,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
})
|
{
|
||||||
|
enabled: !!workspace && !!router.query.id,
|
||||||
|
onError: (error) => {
|
||||||
|
showToast({
|
||||||
|
description: error.message,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack minH="100vh">
|
<Stack minH="100vh">
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
import { fetcher } from '@/helpers/fetcher'
|
|
||||||
import { DashboardFolder } from '@typebot.io/prisma'
|
|
||||||
import useSWR from 'swr'
|
|
||||||
|
|
||||||
export const useFolder = ({
|
|
||||||
folderId,
|
|
||||||
onError,
|
|
||||||
}: {
|
|
||||||
folderId?: string
|
|
||||||
onError: (error: Error) => void
|
|
||||||
}) => {
|
|
||||||
const { data, error, mutate } = useSWR<{ folder: DashboardFolder }, Error>(
|
|
||||||
`/api/folders/${folderId}`,
|
|
||||||
fetcher
|
|
||||||
)
|
|
||||||
if (error) onError(error)
|
|
||||||
return {
|
|
||||||
folder: data?.folder,
|
|
||||||
isLoading: !error && !data,
|
|
||||||
mutate,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
import { fetcher } from '@/helpers/fetcher'
|
|
||||||
import { DashboardFolder } from '@typebot.io/prisma'
|
|
||||||
import { stringify } from 'qs'
|
|
||||||
import useSWR from 'swr'
|
|
||||||
import { env } from '@typebot.io/env'
|
|
||||||
|
|
||||||
export const useFolders = ({
|
|
||||||
parentId,
|
|
||||||
workspaceId,
|
|
||||||
onError,
|
|
||||||
}: {
|
|
||||||
workspaceId?: string
|
|
||||||
parentId?: string
|
|
||||||
onError: (error: Error) => void
|
|
||||||
}) => {
|
|
||||||
const params = stringify({ parentId, workspaceId })
|
|
||||||
const { data, error, mutate } = useSWR<{ folders: DashboardFolder[] }, Error>(
|
|
||||||
workspaceId ? `/api/folders?${params}` : null,
|
|
||||||
fetcher,
|
|
||||||
{
|
|
||||||
dedupingInterval: env.NEXT_PUBLIC_E2E_TEST ? 0 : undefined,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (error) onError(error)
|
|
||||||
return {
|
|
||||||
folders: data?.folders,
|
|
||||||
isLoading: !error && !data,
|
|
||||||
mutate,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
import { DashboardFolder } from '@typebot.io/prisma'
|
|
||||||
import { sendRequest } from '@typebot.io/lib'
|
|
||||||
|
|
||||||
export const createFolderQuery = async (
|
|
||||||
workspaceId: string,
|
|
||||||
folder: Pick<DashboardFolder, 'parentFolderId'>
|
|
||||||
) =>
|
|
||||||
sendRequest<DashboardFolder>({
|
|
||||||
url: `/api/folders`,
|
|
||||||
method: 'POST',
|
|
||||||
body: { ...folder, workspaceId },
|
|
||||||
})
|
|
@ -1,7 +0,0 @@
|
|||||||
import { sendRequest } from '@typebot.io/lib'
|
|
||||||
|
|
||||||
export const deleteFolderQuery = async (id: string) =>
|
|
||||||
sendRequest({
|
|
||||||
url: `/api/folders/${id}`,
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
@ -1,12 +0,0 @@
|
|||||||
import { DashboardFolder } from '@typebot.io/prisma'
|
|
||||||
import { sendRequest } from '@typebot.io/lib'
|
|
||||||
|
|
||||||
export const updateFolderQuery = async (
|
|
||||||
id: string,
|
|
||||||
folder: Partial<DashboardFolder>
|
|
||||||
) =>
|
|
||||||
sendRequest({
|
|
||||||
url: `/api/folders/${id}`,
|
|
||||||
method: 'PATCH',
|
|
||||||
body: folder,
|
|
||||||
})
|
|
6
apps/builder/src/features/telemetry/api/router.ts
Normal file
6
apps/builder/src/features/telemetry/api/router.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { router } from '@/helpers/server/trpc'
|
||||||
|
import { trackClientEvents } from './trackClientEvents'
|
||||||
|
|
||||||
|
export const telemetryRouter = router({
|
||||||
|
trackClientEvents,
|
||||||
|
})
|
95
apps/builder/src/features/telemetry/api/trackClientEvents.ts
Normal file
95
apps/builder/src/features/telemetry/api/trackClientEvents.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import prisma from '@typebot.io/lib/prisma'
|
||||||
|
import { getUserRoleInWorkspace } from '@/features/workspace/helpers/getUserRoleInWorkspace'
|
||||||
|
import { WorkspaceRole } from '@typebot.io/prisma'
|
||||||
|
import { isWriteTypebotForbidden } from '@/features/typebot/helpers/isWriteTypebotForbidden'
|
||||||
|
import { trackEvents } from '@typebot.io/lib/telemetry/trackEvents'
|
||||||
|
import { clientSideCreateEventSchema } from '@typebot.io/schemas'
|
||||||
|
|
||||||
|
export const trackClientEvents = authenticatedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
events: z.array(clientSideCreateEventSchema),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
message: z.literal('success'),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ input: { events }, ctx: { user } }) => {
|
||||||
|
const workspaces = await prisma.workspace.findMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: events
|
||||||
|
.filter((event) => 'workspaceId' in event)
|
||||||
|
.map((event) => (event as { workspaceId: string }).workspaceId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
members: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const typebots = await prisma.typebot.findMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: events
|
||||||
|
.filter((event) => 'typebotId' in event)
|
||||||
|
.map((event) => (event as { typebotId: string }).typebotId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
workspaceId: true,
|
||||||
|
workspace: {
|
||||||
|
select: {
|
||||||
|
isSuspended: true,
|
||||||
|
isPastDue: true,
|
||||||
|
members: {
|
||||||
|
select: {
|
||||||
|
role: true,
|
||||||
|
userId: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
collaborators: {
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
type: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
for (const event of events) {
|
||||||
|
if ('workspaceId' in event) {
|
||||||
|
const workspace = workspaces.find((w) => w.id === event.workspaceId)
|
||||||
|
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 ('typebotId' in event) {
|
||||||
|
const typebot = typebots.find((t) => t.id === event.typebotId)
|
||||||
|
if (!typebot || (await isWriteTypebotForbidden(typebot, user)))
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'Typebot not found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await trackEvents(events.map((e) => ({ ...e, userId: user.id })))
|
||||||
|
|
||||||
|
return { message: 'success' }
|
||||||
|
})
|
@ -0,0 +1,48 @@
|
|||||||
|
import { env } from '@typebot.io/env'
|
||||||
|
import prisma from '@typebot.io/lib/prisma'
|
||||||
|
import { parseGroups, Typebot } from '@typebot.io/schemas'
|
||||||
|
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
existingTypebot: Pick<Typebot, 'id' | 'workspaceId'>
|
||||||
|
userId: string
|
||||||
|
hasFileUploadBlocks: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseTypebotPublishEvents = async ({
|
||||||
|
existingTypebot,
|
||||||
|
userId,
|
||||||
|
hasFileUploadBlocks,
|
||||||
|
}: Props) => {
|
||||||
|
if (!env.NEXT_PUBLIC_POSTHOG_KEY) return []
|
||||||
|
const events = []
|
||||||
|
const existingPublishedTypebot = await prisma.publicTypebot.findFirst({
|
||||||
|
where: {
|
||||||
|
typebotId: existingTypebot.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
version: true,
|
||||||
|
groups: true,
|
||||||
|
settings: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const isPublishingFileUploadBlockForTheFirstTime =
|
||||||
|
hasFileUploadBlocks &&
|
||||||
|
(!existingPublishedTypebot ||
|
||||||
|
!parseGroups(existingPublishedTypebot.groups, {
|
||||||
|
typebotVersion: existingPublishedTypebot.version,
|
||||||
|
}).some((group) =>
|
||||||
|
group.blocks.some((block) => block.type === InputBlockType.FILE)
|
||||||
|
))
|
||||||
|
|
||||||
|
if (isPublishingFileUploadBlockForTheFirstTime)
|
||||||
|
events.push({
|
||||||
|
name: 'File upload block published',
|
||||||
|
workspaceId: existingTypebot.workspaceId,
|
||||||
|
typebotId: existingTypebot.id,
|
||||||
|
userId,
|
||||||
|
} as const)
|
||||||
|
|
||||||
|
return events
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import { getAuthOptions } from '@/pages/api/auth/[...nextauth]'
|
||||||
|
import prisma from '@typebot.io/lib/prisma'
|
||||||
|
import { trackEvents } from '@typebot.io/lib/telemetry/trackEvents'
|
||||||
|
import { User } from '@typebot.io/schemas'
|
||||||
|
import { GetServerSidePropsContext } from 'next'
|
||||||
|
import { getServerSession } from 'next-auth'
|
||||||
|
|
||||||
|
export const trackAnalyticsPageView = async (
|
||||||
|
context: GetServerSidePropsContext
|
||||||
|
) => {
|
||||||
|
const typebotId = context.params?.typebotId as string | undefined
|
||||||
|
if (!typebotId) return
|
||||||
|
const typebot = await prisma.typebot.findUnique({
|
||||||
|
where: { id: typebotId },
|
||||||
|
select: { workspaceId: true },
|
||||||
|
})
|
||||||
|
if (!typebot) return
|
||||||
|
const session = await getServerSession(
|
||||||
|
context.req,
|
||||||
|
context.res,
|
||||||
|
getAuthOptions({})
|
||||||
|
)
|
||||||
|
await trackEvents([
|
||||||
|
{
|
||||||
|
name: 'Analytics visited',
|
||||||
|
typebotId,
|
||||||
|
userId: (session?.user as User).id,
|
||||||
|
workspaceId: typebot.workspaceId,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
@ -10,6 +10,9 @@ import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
|||||||
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
|
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
|
||||||
import { useTranslate } from '@tolgee/react'
|
import { useTranslate } from '@tolgee/react'
|
||||||
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
|
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
|
||||||
|
import { trpc } from '@/lib/trpc'
|
||||||
|
import { env } from '@typebot.io/env'
|
||||||
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isBrandingEnabled: boolean
|
isBrandingEnabled: boolean
|
||||||
@ -27,8 +30,12 @@ export const GeneralSettings = ({
|
|||||||
const { t } = useTranslate()
|
const { t } = useTranslate()
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
const { workspace } = useWorkspace()
|
const { workspace } = useWorkspace()
|
||||||
|
const { typebot } = useTypebot()
|
||||||
const isWorkspaceFreePlan = isFreePlan(workspace)
|
const isWorkspaceFreePlan = isFreePlan(workspace)
|
||||||
|
|
||||||
|
const { mutate: trackClientEvents } =
|
||||||
|
trpc.telemetry.trackClientEvents.useMutation()
|
||||||
|
|
||||||
const handleSelectFont = (font: string) =>
|
const handleSelectFont = (font: string) =>
|
||||||
onGeneralThemeChange({ ...generalTheme, font })
|
onGeneralThemeChange({ ...generalTheme, font })
|
||||||
|
|
||||||
@ -37,6 +44,22 @@ export const GeneralSettings = ({
|
|||||||
|
|
||||||
const updateBranding = () => {
|
const updateBranding = () => {
|
||||||
if (isBrandingEnabled && isWorkspaceFreePlan) return
|
if (isBrandingEnabled && isWorkspaceFreePlan) return
|
||||||
|
if (
|
||||||
|
env.NEXT_PUBLIC_POSTHOG_KEY &&
|
||||||
|
typebot &&
|
||||||
|
workspace &&
|
||||||
|
isBrandingEnabled
|
||||||
|
) {
|
||||||
|
trackClientEvents({
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
name: 'Branding removed',
|
||||||
|
typebotId: typebot.id,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
onBrandingChange(!isBrandingEnabled)
|
onBrandingChange(!isBrandingEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/const
|
|||||||
import { computeRiskLevel } from '@typebot.io/radar'
|
import { computeRiskLevel } from '@typebot.io/radar'
|
||||||
import { env } from '@typebot.io/env'
|
import { env } from '@typebot.io/env'
|
||||||
import { trackEvents } from '@typebot.io/lib/telemetry/trackEvents'
|
import { trackEvents } from '@typebot.io/lib/telemetry/trackEvents'
|
||||||
|
import { parseTypebotPublishEvents } from '@/features/telemetry/helpers/parseTypebotPublishEvents'
|
||||||
|
|
||||||
export const publishTypebot = authenticatedProcedure
|
export const publishTypebot = authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
@ -71,19 +72,17 @@ export const publishTypebot = authenticatedProcedure
|
|||||||
)
|
)
|
||||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
|
||||||
|
|
||||||
if (existingTypebot.workspace.plan === Plan.FREE) {
|
const hasFileUploadBlocks = parseGroups(existingTypebot.groups, {
|
||||||
const hasFileUploadBlocks = parseGroups(existingTypebot.groups, {
|
typebotVersion: existingTypebot.version,
|
||||||
typebotVersion: existingTypebot.version,
|
}).some((group) =>
|
||||||
}).some((group) =>
|
group.blocks.some((block) => block.type === InputBlockType.FILE)
|
||||||
group.blocks.some((block) => block.type === InputBlockType.FILE)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if (hasFileUploadBlocks)
|
if (hasFileUploadBlocks && existingTypebot.workspace.plan === Plan.FREE)
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'BAD_REQUEST',
|
code: 'BAD_REQUEST',
|
||||||
message: "File upload blocks can't be published on the free plan",
|
message: "File upload blocks can't be published on the free plan",
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
const typebotWasVerified =
|
const typebotWasVerified =
|
||||||
existingTypebot.riskLevel === -1 || existingTypebot.workspace.isVerified
|
existingTypebot.riskLevel === -1 || existingTypebot.workspace.isVerified
|
||||||
@ -133,6 +132,12 @@ export const publishTypebot = authenticatedProcedure
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const publishEvents = await parseTypebotPublishEvents({
|
||||||
|
existingTypebot,
|
||||||
|
userId: user.id,
|
||||||
|
hasFileUploadBlocks,
|
||||||
|
})
|
||||||
|
|
||||||
if (existingTypebot.publishedTypebot)
|
if (existingTypebot.publishedTypebot)
|
||||||
await prisma.publicTypebot.updateMany({
|
await prisma.publicTypebot.updateMany({
|
||||||
where: {
|
where: {
|
||||||
@ -175,6 +180,7 @@ export const publishTypebot = authenticatedProcedure
|
|||||||
})
|
})
|
||||||
|
|
||||||
await trackEvents([
|
await trackEvents([
|
||||||
|
...publishEvents,
|
||||||
{
|
{
|
||||||
name: 'Typebot published',
|
name: 'Typebot published',
|
||||||
workspaceId: existingTypebot.workspaceId,
|
workspaceId: existingTypebot.workspaceId,
|
||||||
|
@ -6,6 +6,7 @@ import { internalWhatsAppRouter } from '@/features/whatsapp/router'
|
|||||||
import { zemanticAiRouter } from '@/features/blocks/integrations/zemanticAi/api/router'
|
import { zemanticAiRouter } from '@/features/blocks/integrations/zemanticAi/api/router'
|
||||||
import { forgeRouter } from '@/features/forge/api/router'
|
import { forgeRouter } from '@/features/forge/api/router'
|
||||||
import { googleSheetsRouter } from '@/features/blocks/integrations/googleSheets/api/router'
|
import { googleSheetsRouter } from '@/features/blocks/integrations/googleSheets/api/router'
|
||||||
|
import { telemetryRouter } from '@/features/telemetry/api/router'
|
||||||
|
|
||||||
export const internalRouter = router({
|
export const internalRouter = router({
|
||||||
getAppVersionProcedure,
|
getAppVersionProcedure,
|
||||||
@ -15,6 +16,7 @@ export const internalRouter = router({
|
|||||||
zemanticAI: zemanticAiRouter,
|
zemanticAI: zemanticAiRouter,
|
||||||
forge: forgeRouter,
|
forge: forgeRouter,
|
||||||
sheets: googleSheetsRouter,
|
sheets: googleSheetsRouter,
|
||||||
|
telemetry: telemetryRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
export type InternalRouter = typeof internalRouter
|
export type InternalRouter = typeof internalRouter
|
||||||
|
@ -11,6 +11,7 @@ import { analyticsRouter } from '@/features/analytics/api/router'
|
|||||||
import { collaboratorsRouter } from '@/features/collaboration/api/router'
|
import { collaboratorsRouter } from '@/features/collaboration/api/router'
|
||||||
import { customDomainsRouter } from '@/features/customDomains/api/router'
|
import { customDomainsRouter } from '@/features/customDomains/api/router'
|
||||||
import { publicWhatsAppRouter } from '@/features/whatsapp/router'
|
import { publicWhatsAppRouter } from '@/features/whatsapp/router'
|
||||||
|
import { folderRouter } from '@/features/folders/api/router'
|
||||||
|
|
||||||
export const publicRouter = router({
|
export const publicRouter = router({
|
||||||
getLinkedTypebots,
|
getLinkedTypebots,
|
||||||
@ -25,6 +26,7 @@ export const publicRouter = router({
|
|||||||
collaborators: collaboratorsRouter,
|
collaborators: collaboratorsRouter,
|
||||||
customDomains: customDomainsRouter,
|
customDomains: customDomainsRouter,
|
||||||
whatsApp: publicWhatsAppRouter,
|
whatsApp: publicWhatsAppRouter,
|
||||||
|
folders: folderRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
export type PublicRouter = typeof publicRouter
|
export type PublicRouter = typeof publicRouter
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
} from '@typebot.io/lib/api'
|
} from '@typebot.io/lib/api'
|
||||||
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
|
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
|
||||||
|
|
||||||
|
// TODO: Delete as it has been migrated to TRPC endpoints
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const user = await getAuthenticatedUser(req, res)
|
const user = await getAuthenticatedUser(req, res)
|
||||||
if (!user) return notAuthenticated(res)
|
if (!user) return notAuthenticated(res)
|
||||||
|
@ -4,6 +4,7 @@ import { NextApiRequest, NextApiResponse } from 'next'
|
|||||||
import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api'
|
import { methodNotAllowed, notAuthenticated } from '@typebot.io/lib/api'
|
||||||
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
|
import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser'
|
||||||
|
|
||||||
|
// TODO: Delete as it has been migrated to TRPC endpoints
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const user = await getAuthenticatedUser(req, res)
|
const user = await getAuthenticatedUser(req, res)
|
||||||
if (!user) return notAuthenticated(res)
|
if (!user) return notAuthenticated(res)
|
||||||
|
@ -1,5 +1,21 @@
|
|||||||
|
import { GetServerSidePropsContext } from 'next'
|
||||||
import ResultsPage from '../results'
|
import ResultsPage from '../results'
|
||||||
|
import { env } from '@typebot.io/env'
|
||||||
|
import { trackAnalyticsPageView } from '@/features/telemetry/helpers/trackAnalyticsPageView'
|
||||||
|
|
||||||
const AnalyticsPage = ResultsPage
|
const AnalyticsPage = ResultsPage
|
||||||
|
|
||||||
|
export const getServerSideProps = async (
|
||||||
|
context: GetServerSidePropsContext
|
||||||
|
) => {
|
||||||
|
if (!env.NEXT_PUBLIC_POSTHOG_KEY)
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
}
|
||||||
|
await trackAnalyticsPageView(context)
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default AnalyticsPage
|
export default AnalyticsPage
|
||||||
|
4
apps/docs/api-reference/folder/create.mdx
Normal file
4
apps/docs/api-reference/folder/create.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Create a folder'
|
||||||
|
openapi: POST /v1/folders
|
||||||
|
---
|
4
apps/docs/api-reference/folder/delete.mdx
Normal file
4
apps/docs/api-reference/folder/delete.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Delete a folder'
|
||||||
|
openapi: DELETE /v1/folders/{folderId}
|
||||||
|
---
|
4
apps/docs/api-reference/folder/get.mdx
Normal file
4
apps/docs/api-reference/folder/get.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Get a folder'
|
||||||
|
openapi: GET /v1/folders/{folderId}
|
||||||
|
---
|
4
apps/docs/api-reference/folder/list.mdx
Normal file
4
apps/docs/api-reference/folder/list.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'List folders'
|
||||||
|
openapi: GET /v1/folders
|
||||||
|
---
|
4
apps/docs/api-reference/folder/update.mdx
Normal file
4
apps/docs/api-reference/folder/update.mdx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Update a folder'
|
||||||
|
openapi: PATCH /v1/folders/{folderId}
|
||||||
|
---
|
@ -288,6 +288,16 @@
|
|||||||
"api-reference/results/list-logs"
|
"api-reference/results/list-logs"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "Folder",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/folder/list",
|
||||||
|
"api-reference/folder/get",
|
||||||
|
"api-reference/folder/create",
|
||||||
|
"api-reference/folder/update",
|
||||||
|
"api-reference/folder/delete"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Workspace",
|
"group": "Workspace",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
@ -14697,6 +14697,677 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/v1/folders/{folderId}": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "folders-getFolder",
|
||||||
|
"summary": "Get folder",
|
||||||
|
"tags": [
|
||||||
|
"Folder"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Authorization": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "folderId",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "workspaceId",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"folder": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"parentFolderId": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
"name",
|
||||||
|
"parentFolderId",
|
||||||
|
"workspaceId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"folder"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid input data",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.BAD_REQUEST"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Authorization not provided",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.UNAUTHORIZED"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Insufficient access",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.FORBIDDEN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.NOT_FOUND"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.INTERNAL_SERVER_ERROR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patch": {
|
||||||
|
"operationId": "folders-updateFolder",
|
||||||
|
"summary": "Update a folder",
|
||||||
|
"tags": [
|
||||||
|
"Folder"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Authorization": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "folderId",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"folder": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"parentFolderId": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"workspaceId",
|
||||||
|
"folder"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"folder": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"parentFolderId": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
"name",
|
||||||
|
"parentFolderId",
|
||||||
|
"workspaceId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"folder"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid input data",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.BAD_REQUEST"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Authorization not provided",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.UNAUTHORIZED"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Insufficient access",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.FORBIDDEN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.NOT_FOUND"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.INTERNAL_SERVER_ERROR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"operationId": "folders-deleteFolder",
|
||||||
|
"summary": "Delete a folder",
|
||||||
|
"tags": [
|
||||||
|
"Folder"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Authorization": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "folderId",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "workspaceId",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"folder": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"parentFolderId": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
"name",
|
||||||
|
"parentFolderId",
|
||||||
|
"workspaceId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"folder"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid input data",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.BAD_REQUEST"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Authorization not provided",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.UNAUTHORIZED"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Insufficient access",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.FORBIDDEN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.NOT_FOUND"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.INTERNAL_SERVER_ERROR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/v1/folders": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "folders-createFolder",
|
||||||
|
"summary": "Create a folder",
|
||||||
|
"tags": [
|
||||||
|
"Folder"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Authorization": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"folderName": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "New folder"
|
||||||
|
},
|
||||||
|
"parentFolderId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"workspaceId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"folder": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"parentFolderId": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
"name",
|
||||||
|
"parentFolderId",
|
||||||
|
"workspaceId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"folder"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid input data",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.BAD_REQUEST"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Authorization not provided",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.UNAUTHORIZED"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Insufficient access",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.FORBIDDEN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.INTERNAL_SERVER_ERROR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"operationId": "folders-listFolders",
|
||||||
|
"summary": "List folders",
|
||||||
|
"tags": [
|
||||||
|
"Folder"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Authorization": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "workspaceId",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "parentFolderId",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"folders": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"parentFolderId": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
"name",
|
||||||
|
"parentFolderId",
|
||||||
|
"workspaceId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"folders"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid input data",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.BAD_REQUEST"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Authorization not provided",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.UNAUTHORIZED"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Insufficient access",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.FORBIDDEN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.NOT_FOUND"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/error.INTERNAL_SERVER_ERROR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
|
13
packages/schemas/features/folder.ts
Normal file
13
packages/schemas/features/folder.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { DashboardFolder } from '@typebot.io/prisma'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const folderSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
name: z.string(),
|
||||||
|
parentFolderId: z.string().nullable(),
|
||||||
|
workspaceId: z.string(),
|
||||||
|
}) satisfies z.ZodType<DashboardFolder>
|
||||||
|
|
||||||
|
export type Folder = z.infer<typeof folderSchema>
|
@ -144,6 +144,32 @@ export const workspaceNotPastDueEventSchema = workspaceEvent.merge(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const removedBrandingEventSchema = typebotEvent.merge(
|
||||||
|
z.object({
|
||||||
|
name: z.literal('Branding removed'),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const createdFolderEventSchema = workspaceEvent.merge(
|
||||||
|
z.object({
|
||||||
|
name: z.literal('Folder created'),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const publishedFileUploadBlockEventSchema = typebotEvent.merge(
|
||||||
|
z.object({
|
||||||
|
name: z.literal('File upload block published'),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const visitedAnalyticsEventSchema = typebotEvent.merge(
|
||||||
|
z.object({
|
||||||
|
name: z.literal('Analytics visited'),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const clientSideEvents = [removedBrandingEventSchema] as const
|
||||||
|
|
||||||
export const eventSchema = z.discriminatedUnion('name', [
|
export const eventSchema = z.discriminatedUnion('name', [
|
||||||
workspaceCreatedEventSchema,
|
workspaceCreatedEventSchema,
|
||||||
userCreatedEventSchema,
|
userCreatedEventSchema,
|
||||||
@ -159,6 +185,14 @@ export const eventSchema = z.discriminatedUnion('name', [
|
|||||||
userUpdatedEventSchema,
|
userUpdatedEventSchema,
|
||||||
customDomainAddedEventSchema,
|
customDomainAddedEventSchema,
|
||||||
whatsAppCredentialsCreatedEventSchema,
|
whatsAppCredentialsCreatedEventSchema,
|
||||||
|
createdFolderEventSchema,
|
||||||
|
publishedFileUploadBlockEventSchema,
|
||||||
|
visitedAnalyticsEventSchema,
|
||||||
|
...clientSideEvents,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
export const clientSideCreateEventSchema = removedBrandingEventSchema.omit({
|
||||||
|
userId: true,
|
||||||
|
})
|
||||||
|
|
||||||
export type TelemetryEvent = z.infer<typeof eventSchema>
|
export type TelemetryEvent = z.infer<typeof eventSchema>
|
||||||
|
@ -14,3 +14,5 @@ export * from './features/items'
|
|||||||
export * from './features/analytics'
|
export * from './features/analytics'
|
||||||
export * from './features/events'
|
export * from './features/events'
|
||||||
export * from './features/user/schema'
|
export * from './features/user/schema'
|
||||||
|
export * from './features/folder'
|
||||||
|
export * from './features/telemetry'
|
||||||
|
Reference in New Issue
Block a user