diff --git a/.env.example b/.env.example index b839bc99f..71e2da2a6 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,8 @@ DATABASE_URL=postgresql://username:password@host:5450/typebot?schema=public +SECRET=secret +NEXTAUTH_URL=http://localhost:3000 + # Used for email auth and email notifications EMAIL_SERVER_USER=username EMAIL_SERVER_PASSWORD=password diff --git a/README.md b/README.md index 41244ba01..53b76bdf8 100644 --- a/README.md +++ b/README.md @@ -33,26 +33,10 @@ 3. Copy `.env.example` to `.env` 4. Configure environment variables in the `.env` file. -5. Setup the database +5. Run the applications ```sh - yarn dev:setup - ``` - -6. Run the applications - - ```sh - yarn dev:builder - ``` - - ```sh - yarn dev:viewer - ``` - -7. Open [Prisma Studio](https://www.prisma.io/studio) to look at or modify the database content - - ```sh - yarn db:inspect + yarn dx ``` ## Deployment diff --git a/apps/builder/assets/icons.tsx b/apps/builder/assets/icons.tsx index 5a9d34e86..13ea30c81 100644 --- a/apps/builder/assets/icons.tsx +++ b/apps/builder/assets/icons.tsx @@ -1 +1,72 @@ import { IconProps, Icon } from '@chakra-ui/react' + +const featherIconsBaseProps: IconProps = { + fill: 'none', + stroke: 'currentColor', + strokeWidth: '2px', + strokeLinecap: 'round', + strokeLinejoin: 'round', +} +export const SettingsIcon = (props: IconProps) => ( + + + + +) + +export const LogOutIcon = (props: IconProps) => ( + + + + + +) + +export const ChevronLeftIcon = (props: IconProps) => ( + + + +) + +export const PlusIcon = (props: IconProps) => ( + + + + +) + +export const FolderIcon = (props: IconProps) => ( + + + +) + +export const MoreVerticalIcon = (props: IconProps) => ( + + + + + +) + +export const GlobeIcon = (props: IconProps) => ( + + + + + +) + +export const ToolIcon = (props: IconProps) => ( + + + +) + +export const FolderPlusIcon = (props: IconProps) => ( + + + + + +) diff --git a/apps/builder/components/HOC/withUser.tsx b/apps/builder/components/HOC/withUser.tsx index 7ddde1a20..5526e3484 100644 --- a/apps/builder/components/HOC/withUser.tsx +++ b/apps/builder/components/HOC/withUser.tsx @@ -1,17 +1,12 @@ import { useSession } from 'next-auth/react' import { useRouter } from 'next/router' import { useEffect } from 'react' -import { User } from '@typebot/prisma' - -export type withAuthProps = { - user?: User -} const withAuth = - (WrappedComponent: ({ user }: withAuthProps) => JSX.Element) => - (props: JSX.IntrinsicAttributes & withAuthProps) => { + (WrappedComponent: (props: any) => JSX.Element) => + (props: JSX.IntrinsicAttributes) => { const router = useRouter() - const { data: session, status } = useSession() + const { status } = useSession() useEffect(() => { if (!router.isReady) return @@ -19,9 +14,7 @@ const withAuth = if (status === 'unauthenticated') router.replace('/signin') }, [status, router]) - return ( - - ) + return } export default withAuth diff --git a/apps/builder/components/MoreButton.tsx b/apps/builder/components/MoreButton.tsx new file mode 100644 index 000000000..3d1ab9645 --- /dev/null +++ b/apps/builder/components/MoreButton.tsx @@ -0,0 +1,28 @@ +import { + ButtonProps, + IconButton, + Menu, + MenuButton, + MenuList, +} from '@chakra-ui/react' +import { MoreVerticalIcon } from 'assets/icons' +import { ReactNode } from 'react' + +type Props = { children: ReactNode } & ButtonProps + +export const MoreButton = ({ children, ...props }: Props) => { + return ( + + } + onClick={(e) => e.stopPropagation()} + colorScheme="blue" + variant="ghost" + size="lg" + {...props} + /> + {children} + + ) +} diff --git a/apps/builder/components/dashboard/DashboardHeader.tsx b/apps/builder/components/dashboard/DashboardHeader.tsx new file mode 100644 index 000000000..b4ff8d894 --- /dev/null +++ b/apps/builder/components/dashboard/DashboardHeader.tsx @@ -0,0 +1,74 @@ +import React from 'react' +import { + Menu, + MenuButton, + MenuList, + MenuItem, + Text, + HStack, + Flex, + Avatar, + SkeletonCircle, + Skeleton, +} from '@chakra-ui/react' +import { TypebotLogo } from 'assets/logos' +import { useUser } from 'services/user' +import { NextChakraLink } from 'components/nextChakra/NextChakraLink' +import { LogOutIcon, SettingsIcon } from 'assets/icons' +import { signOut } from 'next-auth/react' + +export const DashboardHeader = () => { + const user = useUser() + + const handleLogOut = () => { + signOut() + } + + return ( + + + + + + + + + + {user?.name} + + + + + + + + } + > + My account + + }> + Log out + + + + + + ) +} diff --git a/apps/builder/components/dashboard/FolderContent.tsx b/apps/builder/components/dashboard/FolderContent.tsx new file mode 100644 index 000000000..ae8c233da --- /dev/null +++ b/apps/builder/components/dashboard/FolderContent.tsx @@ -0,0 +1,181 @@ +import { DashboardFolder, Typebot } from '.prisma/client' +import { + Button, + Flex, + Heading, + HStack, + Skeleton, + Stack, + useToast, + Wrap, +} from '@chakra-ui/react' +import { + DndContext, + DragEndEvent, + DragOverlay, + DragStartEvent, + MouseSensor, + useSensor, + useSensors, +} from '@dnd-kit/core' +import { FolderPlusIcon } from 'assets/icons' +import React, { useState } from 'react' +import { createFolder, useFolders } from 'services/folders' +import { updateTypebot, useTypebots } from 'services/typebots' +import { BackButton } from './FolderContent/BackButton' +import { CreateBotButton } from './FolderContent/CreateBotButton' +import { ButtonSkeleton, FolderButton } from './FolderContent/FolderButton' +import { TypebotButton } from './FolderContent/TypebotButton' +import { TypebotCardOverlay } from './FolderContent/TypebotButtonOverlay' + +type Props = { folder: DashboardFolder | null } + +export const FolderContent = ({ folder }: Props) => { + const [isCreatingFolder, setIsCreatingFolder] = useState(false) + const [draggedTypebot, setDraggedTypebot] = useState() + const sensors = useSensors( + useSensor(MouseSensor, { + activationConstraint: { + delay: 100, + tolerance: 300, + }, + }) + ) + const toast = useToast({ + position: 'top-right', + status: 'error', + }) + const { + folders, + isLoading: isFolderLoading, + mutate: mutateFolders, + } = useFolders({ + parentId: folder?.id, + onError: (error) => { + toast({ title: "Couldn't fetch folders", description: error.message }) + }, + }) + + const { + typebots, + isLoading: isTypebotLoading, + mutate: mutateTypebots, + } = useTypebots({ + folderId: folder?.id, + onError: (error) => { + toast({ title: "Couldn't fetch typebots", description: error.message }) + }, + }) + + const handleDragStart = (event: DragStartEvent) => { + if (!typebots) return + setDraggedTypebot(typebots.find((c) => c.id === event.active.id)) + } + + const handleDragEnd = async (event: DragEndEvent) => { + if (!typebots) return + const { over } = event + if (over?.id && draggedTypebot?.id) + await moveTypebotToFolder(draggedTypebot.id, over.id) + setDraggedTypebot(undefined) + } + + const moveTypebotToFolder = async (typebotId: string, folderId: string) => { + if (!typebots) return + const { error } = await updateTypebot(typebotId, { + folderId: folderId === 'root' ? null : folderId, + }) + if (error) toast({ description: error.message }) + mutateTypebots({ typebots: typebots.filter((t) => t.id !== typebotId) }) + } + + const handleCreateFolder = async () => { + if (!folders) return + setIsCreatingFolder(true) + const { error, data: newFolder } = await createFolder({ + parentFolderId: folder?.id ?? null, + }) + setIsCreatingFolder(false) + if (error) + return toast({ title: 'An error occured', description: error.message }) + if (newFolder) mutateFolders({ folders: [...folders, newFolder] }) + } + + const handleTypebotDeleted = (deletedId: string) => { + if (!typebots) return + mutateTypebots({ typebots: typebots.filter((t) => t.id !== deletedId) }) + } + + 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)), + }) + } + + return ( + + + + + {folder?.name} + + + + {folder && } + + + + + {isFolderLoading && } + {folders && + folders.map((folder) => ( + handleFolderDeleted(folder.id)} + onFolderRenamed={(newName: string) => + handleFolderRenamed(folder.id, newName) + } + /> + ))} + {isTypebotLoading && } + {typebots && + typebots.map((typebot) => ( + handleTypebotDeleted(typebot.id)} + /> + ))} + + {draggedTypebot && ( + + )} + + + + + + + ) +} diff --git a/apps/builder/components/dashboard/FolderContent/BackButton.tsx b/apps/builder/components/dashboard/FolderContent/BackButton.tsx new file mode 100644 index 000000000..4f0c7e748 --- /dev/null +++ b/apps/builder/components/dashboard/FolderContent/BackButton.tsx @@ -0,0 +1,24 @@ +import { Button } from '@chakra-ui/react' +import { useDroppable } from '@dnd-kit/core' +import { ChevronLeftIcon } from 'assets/icons' +import { NextChakraLink } from 'components/nextChakra/NextChakraLink' +import React from 'react' + +export const BackButton = ({ id }: { id: string | null }) => { + const { setNodeRef, isOver } = useDroppable({ + id: id?.toString() ?? 'root', + }) + return ( + + ) +} diff --git a/apps/builder/components/dashboard/FolderContent/CreateBotButton.tsx b/apps/builder/components/dashboard/FolderContent/CreateBotButton.tsx new file mode 100644 index 000000000..c12a0d046 --- /dev/null +++ b/apps/builder/components/dashboard/FolderContent/CreateBotButton.tsx @@ -0,0 +1,41 @@ +import { Button, ButtonProps, Text, VStack } from '@chakra-ui/react' +import { PlusIcon } from 'assets/icons' +import { useRouter } from 'next/router' +import React from 'react' + +export const CreateBotButton = ({ + folderId, + ...props +}: { folderId?: string } & ButtonProps) => { + const router = useRouter() + + const handleClick = () => + folderId + ? router.push(`/typebots/create?folderId=${folderId}`) + : router.push('/typebots/create') + + return ( + + ) +} diff --git a/apps/builder/components/dashboard/FolderContent/FolderButton.tsx b/apps/builder/components/dashboard/FolderContent/FolderButton.tsx new file mode 100644 index 000000000..4038a8ec7 --- /dev/null +++ b/apps/builder/components/dashboard/FolderContent/FolderButton.tsx @@ -0,0 +1,158 @@ +import { DashboardFolder } from '.prisma/client' +import { + Button, + Editable, + EditableInput, + EditablePreview, + MenuItem, + useDisclosure, + Text, + VStack, + IconButton, + Menu, + MenuButton, + MenuList, + useToast, + SkeletonText, + SkeletonCircle, + WrapItem, +} from '@chakra-ui/react' +import { useDroppable } from '@dnd-kit/core' +import { FolderIcon, MoreVerticalIcon } from 'assets/icons' +import { ConfirmModal } from 'components/modals/ConfirmModal' +import { useRouter } from 'next/router' +import React from 'react' +import { deleteFolder, updateFolder } from 'services/folders' + +export const FolderButton = ({ + folder, + onFolderDeleted, + onFolderRenamed, +}: { + folder: DashboardFolder + onFolderDeleted: () => void + onFolderRenamed: (newName: string) => void +}) => { + const router = useRouter() + const { setNodeRef, isOver } = useDroppable({ + id: folder.id.toString(), + }) + const { isOpen, onOpen, onClose } = useDisclosure() + const toast = useToast({ + position: 'top-right', + status: 'error', + }) + + const onDeleteClick = async () => { + const { error } = await deleteFolder(folder.id) + return error + ? toast({ + title: "Couldn't delete the folder", + description: error.message, + }) + : onFolderDeleted() + } + + const onRenameSubmit = async (newName: string) => { + if (newName === '' || newName === folder.name) return + const { error } = await updateFolder(folder.id, { name: newName }) + return error + ? toast({ title: 'An error occured', description: error.message }) + : onFolderRenamed(newName) + } + + const handleClick = () => { + router.push(`/typebots/folders/${folder.id}`) + } + + return ( + + ) +} + +export const ButtonSkeleton = () => ( + +) diff --git a/apps/builder/components/dashboard/FolderContent/TypebotButton.tsx b/apps/builder/components/dashboard/FolderContent/TypebotButton.tsx new file mode 100644 index 000000000..f59b7f2ba --- /dev/null +++ b/apps/builder/components/dashboard/FolderContent/TypebotButton.tsx @@ -0,0 +1,133 @@ +import React from 'react' +import { + Button, + Flex, + MenuItem, + Text, + useDisclosure, + useToast, + VStack, + WrapItem, +} from '@chakra-ui/react' +import { useDraggable } from '@dnd-kit/core' +import { useRouter } from 'next/router' +import { Typebot } from '@typebot/prisma' +import { isMobile } from 'services/utils' +import { MoreButton } from 'components/MoreButton' +import { ConfirmModal } from 'components/modals/ConfirmModal' +import { GlobeIcon, ToolIcon } from 'assets/icons' +import { deleteTypebot, duplicateTypebot } from 'services/typebots' + +type ChatbotCardProps = { + typebot: Typebot + onTypebotDeleted: () => void +} + +export const TypebotButton = ({ + typebot, + onTypebotDeleted, +}: ChatbotCardProps) => { + const router = useRouter() + const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ + id: typebot.id.toString(), + }) + const { + isOpen: isDeleteOpen, + onOpen: onDeleteOpen, + onClose: onDeleteClose, + } = useDisclosure() + + const toast = useToast({ + position: 'top-right', + status: 'error', + }) + + const handleTypebotClick = () => { + router.push( + isMobile + ? `/typebots/${typebot.id}/results/responses` + : `/typebots/${typebot.id}/edit` + ) + } + + const handleDeleteTypebotClick = async () => { + const { error } = await deleteTypebot(typebot.id) + if (error) + return toast({ + title: "Couldn't delete typebot", + description: error.message, + }) + onTypebotDeleted() + } + + const handleDuplicateClick = async () => { + const { data: createdTypebot, error } = await duplicateTypebot(typebot) + if (error) + return toast({ + title: "Couldn't duplicate typebot", + description: error.message, + }) + if (createdTypebot) router.push(`/typebots/${createdTypebot?.id}`) + } + + return ( + + ) +} diff --git a/apps/builder/components/dashboard/FolderContent/TypebotButtonOverlay.tsx b/apps/builder/components/dashboard/FolderContent/TypebotButtonOverlay.tsx new file mode 100644 index 000000000..f5152aad5 --- /dev/null +++ b/apps/builder/components/dashboard/FolderContent/TypebotButtonOverlay.tsx @@ -0,0 +1,44 @@ +import { Button, Flex, Text, VStack } from '@chakra-ui/react' +import { Typebot } from '.prisma/client' +import { GlobeIcon, ToolIcon } from 'assets/icons' + +type Props = { + typebot: Typebot +} + +export const TypebotCardOverlay = ({ typebot }: Props) => { + return ( +
+ +
+ ) +} diff --git a/apps/builder/components/modals/ConfirmModal.tsx b/apps/builder/components/modals/ConfirmModal.tsx new file mode 100644 index 000000000..5f53385ed --- /dev/null +++ b/apps/builder/components/modals/ConfirmModal.tsx @@ -0,0 +1,77 @@ +import { useRef, useState } from 'react' +import { + AlertDialog, + AlertDialogBody, + AlertDialogContent, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogOverlay, + Button, +} from '@chakra-ui/react' + +type ConfirmDeleteModalProps = { + isOpen: boolean + onConfirm: () => Promise + onClose: () => void + message: JSX.Element + title?: string + confirmButtonLabel: string + confirmButtonColor?: 'blue' | 'red' +} + +export const ConfirmModal = ({ + title, + message, + isOpen, + onClose, + confirmButtonLabel, + onConfirm, + confirmButtonColor = 'red', +}: ConfirmDeleteModalProps) => { + const [confirmLoading, setConfirmLoading] = useState(false) + const cancelRef = useRef(null) + + const onConfirmClick = async () => { + setConfirmLoading(true) + try { + await onConfirm() + } catch (e) { + setConfirmLoading(false) + return setConfirmLoading(false) + } + setConfirmLoading(false) + onClose() + } + + return ( + + + + + {title ?? 'Are you sure?'} + + + {message} + + + + + + + + + ) +} diff --git a/apps/builder/cypress/plugins/database.ts b/apps/builder/cypress/plugins/database.ts new file mode 100644 index 000000000..09769e919 --- /dev/null +++ b/apps/builder/cypress/plugins/database.ts @@ -0,0 +1,30 @@ +import { PrismaClient } from '.prisma/client' + +const prisma = new PrismaClient() + +const teardownTestData = async () => prisma.user.deleteMany() + +export const seedDb = async () => { + await teardownTestData() + await createUsers() + await createFolders() + return createTypebots() +} + +const createUsers = () => + prisma.user.createMany({ + data: [ + { id: 'test1', email: 'test1@gmail.com', emailVerified: new Date() }, + { id: 'test2', email: 'test2@gmail.com', emailVerified: new Date() }, + ], + }) + +const createFolders = () => + prisma.dashboardFolder.createMany({ + data: [{ ownerId: 'test2', name: 'Folder #1', id: 'folder1' }], + }) + +const createTypebots = () => + prisma.typebot.createMany({ + data: [{ name: 'Typebot #1', ownerId: 'test2' }], + }) diff --git a/apps/builder/cypress/plugins/index.ts b/apps/builder/cypress/plugins/index.ts index d9f811496..0723ef984 100644 --- a/apps/builder/cypress/plugins/index.ts +++ b/apps/builder/cypress/plugins/index.ts @@ -3,6 +3,7 @@ import { FacebookSocialLogin, GoogleSocialLogin, } from 'cypress-social-logins/src/Plugins' +import { seedDb } from './database' /// /** @@ -14,6 +15,7 @@ const handler = (on: any) => { GoogleSocialLogin: GoogleSocialLogin, FacebookSocialLogin: FacebookSocialLogin, GitHubSocialLogin: GitHubSocialLogin, + seed: seedDb, }) } diff --git a/apps/builder/cypress/support/commands.ts b/apps/builder/cypress/support/commands.ts index b7c5a49d4..c1b1a848e 100644 --- a/apps/builder/cypress/support/commands.ts +++ b/apps/builder/cypress/support/commands.ts @@ -1,37 +1,93 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add('login', (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) -import '@testing-library/cypress/add-commands' +import { signIn, signOut } from 'next-auth/react' -Cypress.Commands.add('logOutByApi', () => - cy - .request('GET', `${Cypress.env('SITE_NAME')}/api/auth/csrf/login`) - .its('body') - .then((result) => { - cy.request('POST', `${Cypress.env('SITE_NAME')}/api/auth/signout`, { - csrfToken: result.csrfToken, +Cypress.Commands.add('signOut', () => { + cy.log(`🔐 Sign out`) + return cy.wrap(signOut({ redirect: false }), { log: false }) +}) + +Cypress.Commands.add('signIn', (email: string) => { + cy.log(`🔐 Sign in as ${email}`) + return cy.wrap(signIn('credentials', { redirect: false, email }), { + log: false, + }) +}) + +Cypress.Commands.add( + 'mouseMoveBy', + { + prevSubject: 'element', + }, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ( + subject: JQuery, + x: number, + y: number, + options?: { delay: number } + ) => { + cy.wrap(subject, { log: false }) + .then((subject) => { + const initialRect = subject.get(0).getBoundingClientRect() + const windowScroll = getDocumentScroll() + + return [subject, initialRect, windowScroll] as const }) - }) + .then(([subject, initialRect, initialWindowScroll]) => { + cy.wrap(subject) + .trigger('mousedown', { force: true }) + .wait(options?.delay || 0, { log: Boolean(options?.delay) }) + .trigger('mousemove', { + force: true, + clientX: Math.floor( + initialRect.left + initialRect.width / 2 + x / 2 + ), + clientY: Math.floor( + initialRect.top + initialRect.height / 2 + y / 2 + ), + }) + .trigger('mousemove', { + force: true, + clientX: Math.floor(initialRect.left + initialRect.width / 2 + x), + clientY: Math.floor(initialRect.top + initialRect.height / 2 + y), + }) + // .wait(1000) + .trigger('mouseup', { force: true }) + .wait(250) + .then((subject: any) => { + const finalRect = subject.get(0).getBoundingClientRect() + const windowScroll = getDocumentScroll() + const windowScrollDelta = { + x: windowScroll.x - initialWindowScroll.x, + y: windowScroll.y - initialWindowScroll.y, + } + + const delta = { + x: Math.round( + finalRect.left - initialRect.left - windowScrollDelta.x + ), + y: Math.round( + finalRect.top - initialRect.top - windowScrollDelta.y + ), + } + + return [subject, { initialRect, finalRect, delta }] as const + }) + }) + } ) + +const getDocumentScroll = () => { + if (document.scrollingElement) { + const { scrollTop, scrollLeft } = document.scrollingElement + + return { + x: scrollTop, + y: scrollLeft, + } + } + + return { + x: 0, + y: 0, + } +} diff --git a/apps/builder/cypress/support/index.ts b/apps/builder/cypress/support/index.ts index 1f4a13437..7e32111c2 100644 --- a/apps/builder/cypress/support/index.ts +++ b/apps/builder/cypress/support/index.ts @@ -13,20 +13,33 @@ // You can read more here: // https://on.cypress.io/configuration // *********************************************************** - -// Import commands.js using ES2015 syntax: +/// declare global { namespace Cypress { interface Chainable { - /** - * Log out using the NextAuth API. - * @example cy.logOutByApi() - */ - logOutByApi(): Chainable> + signOut(): Chainable + signIn(email: string): Chainable + mouseMoveBy( + x: number, + y: number, + options?: { delay: number } + ): Chainable< + [ + Element, + { + initialRect: ClientRect + finalRect: ClientRect + delta: { x: number; y: number } + } + ] + > } } } + +// Import commands.js using ES2015 syntax: +import '@testing-library/cypress/add-commands' import './commands' // Alternatively you can use CommonJS syntax: diff --git a/apps/builder/cypress/tests/auth.ts b/apps/builder/cypress/tests/auth.ts index fb85687f9..882d1c2cb 100644 --- a/apps/builder/cypress/tests/auth.ts +++ b/apps/builder/cypress/tests/auth.ts @@ -1,7 +1,8 @@ describe('SignIn page', () => { beforeEach(() => { - cy.logOutByApi() + cy.signOut() }) + it('can continue with Google', () => { cy.visit('/signin') const username = Cypress.env('GOOGLE_USER') @@ -98,6 +99,6 @@ const exectueSocialLogin = ( }) } cy.visit('/typebots') - cy.findByText(`Hello ${username}`).should('exist') + cy.findByRole('button', { name: 'Create a folder' }).should('exist') }) } diff --git a/apps/builder/cypress/tests/dashboard.ts b/apps/builder/cypress/tests/dashboard.ts new file mode 100644 index 000000000..f9e34eda2 --- /dev/null +++ b/apps/builder/cypress/tests/dashboard.ts @@ -0,0 +1,49 @@ +describe('Dashboard page', () => { + beforeEach(() => { + cy.task('seed') + cy.signOut() + }) + + it('should navigate correctly', () => { + cy.signIn('test1@gmail.com') + cy.visit('/typebots') + createFolder('My folder #1') + cy.findByTestId('folder-button').click() + cy.findByRole('heading', { name: 'My folder #1' }).should('exist') + createFolder('My folder #2') + cy.findByTestId('folder-button').click() + cy.findByRole('heading', { name: 'My folder #2' }).should('exist') + cy.findByRole('link', { name: 'Back' }).click() + cy.findByRole('heading', { name: 'My folder #1' }).should('exist') + cy.findByRole('link', { name: 'Back' }).click() + cy.findByRole('button', { name: 'Show folder menu' }).click() + cy.findByRole('menuitem', { name: 'Delete' }).click() + cy.findByRole('button', { name: 'Delete' }).click() + cy.findByDisplayValue('My folder #2').should('exist') + cy.findByRole('button', { name: 'Show folder menu' }).click() + cy.findByRole('menuitem', { name: 'Delete' }).click() + cy.findByRole('button', { name: 'Delete' }).click() + cy.findByDisplayValue('My folder #2').should('not.exist') + }) + + it('should be droppable', () => { + cy.signIn('test2@gmail.com') + cy.visit('/typebots') + cy.findByTestId('typebot-button').mouseMoveBy(-100, 0, { + delay: 120, + }) + cy.visit('/typebots/folders/folder1') + cy.findByTestId('typebot-button').mouseMoveBy(-300, -100, { + delay: 120, + }) + cy.visit('/typebots') + cy.findByDisplayValue('Folder #1').should('exist') + cy.findByText('Typebot #1').should('exist') + }) +}) + +const createFolder = (folderName: string) => { + cy.findByRole('button', { name: 'Create a folder' }).click({ force: true }) + cy.findByText('New folder').click({ force: true }) + cy.findByDisplayValue('New folder').type(`${folderName}{enter}`) +} diff --git a/apps/builder/cypress/tsconfig.json b/apps/builder/cypress/tsconfig.json index 2a76e8cce..3dbbe026f 100644 --- a/apps/builder/cypress/tsconfig.json +++ b/apps/builder/cypress/tsconfig.json @@ -8,6 +8,7 @@ "target": "es5", "isolatedModules": false, "allowJs": true, - "noEmit": true + "noEmit": true, + "downlevelIteration": true } } diff --git a/apps/builder/libs/prisma.ts b/apps/builder/libs/prisma.ts new file mode 100644 index 000000000..ffc43a367 --- /dev/null +++ b/apps/builder/libs/prisma.ts @@ -0,0 +1,15 @@ +import { PrismaClient } from '@typebot/prisma' + +declare const global: { prisma: PrismaClient } +let prisma: PrismaClient + +if (process.env.NODE_ENV === 'production') { + prisma = new PrismaClient() +} else { + if (!global.prisma) { + global.prisma = new PrismaClient() + } + prisma = global.prisma +} + +export default prisma diff --git a/apps/builder/package.json b/apps/builder/package.json index 8bb1065b7..f621ef810 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -9,17 +9,22 @@ "cypress": "cypress open" }, "dependencies": { + "@chakra-ui/css-reset": "^1.1.1", "@chakra-ui/react": "^1.7.2", + "@dnd-kit/core": "^4.0.3", "@emotion/react": "^11", "@emotion/styled": "^11", "@next-auth/prisma-adapter": "next", + "focus-visible": "^5.2.0", "framer-motion": "^4", "next": "^12.0.4", "next-auth": "beta", "nodemailer": "^6.7.1", "nprogress": "^0.2.0", "react": "^17.0.2", - "react-dom": "^17.0.2" + "react-dom": "^17.0.2", + "swr": "^1.0.1", + "use-debounce": "^7.0.1" }, "devDependencies": { "@testing-library/cypress": "^8.0.2", diff --git a/apps/builder/pages/_app.tsx b/apps/builder/pages/_app.tsx index b6996ddaf..02caa3369 100644 --- a/apps/builder/pages/_app.tsx +++ b/apps/builder/pages/_app.tsx @@ -3,8 +3,9 @@ import { AppProps } from 'next/app' import { SessionProvider } from 'next-auth/react' import { ChakraProvider } from '@chakra-ui/react' import { customTheme } from 'libs/chakra' -import 'assets/styles/routerProgressBar.css' import { useRouterProgressBar } from 'libs/routerProgressBar' +import 'assets/styles/routerProgressBar.css' +import 'focus-visible/dist/focus-visible' const App = ({ Component, pageProps }: AppProps) => { useRouterProgressBar() diff --git a/apps/builder/pages/api/auth/[...nextauth].ts b/apps/builder/pages/api/auth/[...nextauth].ts index 9de52c67d..3329ffd9c 100644 --- a/apps/builder/pages/api/auth/[...nextauth].ts +++ b/apps/builder/pages/api/auth/[...nextauth].ts @@ -1,39 +1,87 @@ import NextAuth from 'next-auth' import { PrismaAdapter } from '@next-auth/prisma-adapter' -import { PrismaClient } from '@typebot/prisma' import EmailProvider from 'next-auth/providers/email' import GitHubProvider from 'next-auth/providers/github' import GoogleProvider from 'next-auth/providers/google' import FacebookProvider from 'next-auth/providers/facebook' +import CredentialsProvider from 'next-auth/providers/credentials' +import prisma from 'libs/prisma' +import { Provider } from 'next-auth/providers' +import { User } from '@typebot/prisma' -const prisma = new PrismaClient() +const providers: Provider[] = [ + EmailProvider({ + server: { + host: process.env.EMAIL_SERVER_HOST, + port: process.env.EMAIL_SERVER_PORT, + auth: { + user: process.env.EMAIL_SERVER_USER, + pass: process.env.EMAIL_SERVER_PASSWORD, + }, + }, + from: process.env.EMAIL_FROM, + }), +] + +if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) + providers.push( + GitHubProvider({ + clientId: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + }) + ) + +if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) + providers.push( + GoogleProvider({ + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + }) + ) + +if (process.env.FACEBOOK_CLIENT_ID && process.env.FACEBOOK_CLIENT_SECRET) + providers.push( + FacebookProvider({ + clientId: process.env.FACEBOOK_CLIENT_ID, + clientSecret: process.env.FACEBOOK_CLIENT_SECRET, + }) + ) + +if (process.env.NODE_ENV !== 'production') + providers.push( + CredentialsProvider({ + name: 'Credentials', + credentials: { + email: { + label: 'Email', + type: 'email', + placeholder: 'email@email.com', + }, + }, + async authorize(credentials) { + const user = await prisma.user.findUnique({ + where: { email: credentials?.email }, + }) + return user + }, + }) + ) export default NextAuth({ adapter: PrismaAdapter(prisma), secret: process.env.SECRET, - providers: [ - EmailProvider({ - server: { - host: process.env.EMAIL_SERVER_HOST, - port: process.env.EMAIL_SERVER_PORT, - auth: { - user: process.env.EMAIL_SERVER_USER, - pass: process.env.EMAIL_SERVER_PASSWORD, - }, - }, - from: process.env.EMAIL_FROM, - }), - GitHubProvider({ - clientId: process.env.GITHUB_CLIENT_ID, - clientSecret: process.env.GITHUB_CLIENT_SECRET, - }), - GoogleProvider({ - clientId: process.env.GOOGLE_CLIENT_ID ?? '', - clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? '', - }), - FacebookProvider({ - clientId: process.env.FACEBOOK_CLIENT_ID ?? '', - clientSecret: process.env.FACEBOOK_CLIENT_SECRET ?? '', - }), - ], + providers, + session: { + strategy: process.env.NODE_ENV === 'production' ? 'database' : 'jwt', + }, + callbacks: { + jwt: async ({ token, user }) => { + user && (token.user = user) + return token + }, + session: async ({ session, token }) => { + if (token.user) session.user = token.user as User + return session + }, + }, }) diff --git a/apps/builder/pages/api/folders.ts b/apps/builder/pages/api/folders.ts new file mode 100644 index 000000000..22d981516 --- /dev/null +++ b/apps/builder/pages/api/folders.ts @@ -0,0 +1,36 @@ +import { DashboardFolder, User } from '@typebot/prisma' +import prisma from 'libs/prisma' +import { NextApiRequest, NextApiResponse } from 'next' +import { getSession } from 'next-auth/react' +import { methodNotAllowed } from 'services/api/utils' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const session = await getSession({ req }) + + if (!session?.user) + return res.status(401).json({ message: 'Not authenticated' }) + + const user = session.user as User + const parentFolderId = req.query.parentId + ? req.query.parentId.toString() + : null + if (req.method === 'GET') { + const folders = await prisma.dashboardFolder.findMany({ + where: { + ownerId: user.id, + parentFolderId, + }, + }) + return res.send({ folders }) + } + if (req.method === 'POST') { + const data = JSON.parse(req.body) as Pick + const folder = await prisma.dashboardFolder.create({ + data: { ...data, ownerId: user.id, name: 'New folder' }, + }) + return res.send(folder) + } + return methodNotAllowed(res) +} + +export default handler diff --git a/apps/builder/pages/api/folders/[id].ts b/apps/builder/pages/api/folders/[id].ts new file mode 100644 index 000000000..75c11b188 --- /dev/null +++ b/apps/builder/pages/api/folders/[id].ts @@ -0,0 +1,37 @@ +import { DashboardFolder } from '@typebot/prisma' +import prisma from 'libs/prisma' +import { NextApiRequest, NextApiResponse } from 'next' +import { getSession } from 'next-auth/react' +import { methodNotAllowed } from 'services/api/utils' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const session = await getSession({ req }) + + if (!session?.user) + return res.status(401).json({ message: 'Not authenticated' }) + + const id = req.query.id.toString() + if (req.method === 'GET') { + const folder = await prisma.dashboardFolder.findUnique({ + where: { id }, + }) + return res.send({ folder }) + } + if (req.method === 'DELETE') { + const folders = await prisma.dashboardFolder.delete({ + where: { id }, + }) + return res.send({ folders }) + } + if (req.method === 'PATCH') { + const data = JSON.parse(req.body) as Partial + const folders = await prisma.dashboardFolder.update({ + where: { id }, + data, + }) + return res.send({ typebots: folders }) + } + return methodNotAllowed(res) +} + +export default handler diff --git a/apps/builder/pages/api/typebots.ts b/apps/builder/pages/api/typebots.ts new file mode 100644 index 000000000..d2237423e --- /dev/null +++ b/apps/builder/pages/api/typebots.ts @@ -0,0 +1,34 @@ +import { Typebot, User } from '@typebot/prisma' +import prisma from 'libs/prisma' +import { NextApiRequest, NextApiResponse } from 'next' +import { getSession } from 'next-auth/react' +import { methodNotAllowed } from 'services/api/utils' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const session = await getSession({ req }) + + if (!session?.user) + return res.status(401).json({ message: 'Not authenticated' }) + + const user = session.user as User + if (req.method === 'GET') { + const folderId = req.query.folderId ? req.query.folderId.toString() : null + const typebots = await prisma.typebot.findMany({ + where: { + ownerId: user.id, + folderId, + }, + }) + return res.send({ typebots }) + } + if (req.method === 'POST') { + const data = JSON.parse(req.body) as Typebot + const typebot = await prisma.typebot.create({ + data: { ...data, ownerId: user.id }, + }) + return res.send(typebot) + } + return methodNotAllowed(res) +} + +export default handler diff --git a/apps/builder/pages/api/typebots/[id].ts b/apps/builder/pages/api/typebots/[id].ts new file mode 100644 index 000000000..ea364c86f --- /dev/null +++ b/apps/builder/pages/api/typebots/[id].ts @@ -0,0 +1,31 @@ +import { Typebot } from '.prisma/client' +import prisma from 'libs/prisma' +import { NextApiRequest, NextApiResponse } from 'next' +import { getSession } from 'next-auth/react' +import { methodNotAllowed } from 'services/api/utils' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const session = await getSession({ req }) + + if (!session?.user) + return res.status(401).json({ message: 'Not authenticated' }) + + const id = req.query.id.toString() + if (req.method === 'DELETE') { + const typebots = await prisma.typebot.delete({ + where: { id }, + }) + return res.send({ typebots }) + } + if (req.method === 'PATCH') { + const data = JSON.parse(req.body) as Partial + const typebots = await prisma.typebot.update({ + where: { id }, + data, + }) + return res.send({ typebots }) + } + return methodNotAllowed(res) +} + +export default handler diff --git a/apps/builder/pages/typebots.tsx b/apps/builder/pages/typebots.tsx new file mode 100644 index 000000000..370b784f8 --- /dev/null +++ b/apps/builder/pages/typebots.tsx @@ -0,0 +1,18 @@ +import withAuth from 'components/HOC/withUser' +import React from 'react' +import { Stack } from '@chakra-ui/layout' +import { DashboardHeader } from 'components/dashboard/DashboardHeader' +import { Seo } from 'components/Seo' +import { FolderContent } from 'components/dashboard/FolderContent' + +const DashboardPage = () => { + return ( + + + + + + ) +} + +export default withAuth(DashboardPage) diff --git a/apps/builder/pages/typebots/create.tsx b/apps/builder/pages/typebots/create.tsx new file mode 100644 index 000000000..c666070c6 --- /dev/null +++ b/apps/builder/pages/typebots/create.tsx @@ -0,0 +1,43 @@ +import React, { useState } from 'react' +import { Button, Stack, useToast } from '@chakra-ui/react' +import { useUser } from 'services/user' +import { useRouter } from 'next/router' +import { Seo } from 'components/Seo' +import { DashboardHeader } from 'components/dashboard/DashboardHeader' +import { createTypebot } from 'services/typebots' + +const TemplatesPage = () => { + const user = useUser() + const router = useRouter() + + const [isLoading, setIsLoading] = useState(false) + + const toast = useToast({ + position: 'top-right', + status: 'error', + title: 'An error occured', + }) + + const handleCreateSubmit = async () => { + if (!user) return + setIsLoading(true) + const { error, data } = await createTypebot({ + folderId: router.query.folderId?.toString() ?? null, + }) + if (error) toast({ description: error.message }) + if (data) router.push(`/typebots/${data.id}`) + setIsLoading(false) + } + + return ( + + + + + + ) +} + +export default TemplatesPage diff --git a/apps/builder/pages/typebots/folders/[id].tsx b/apps/builder/pages/typebots/folders/[id].tsx new file mode 100644 index 000000000..c04416ef7 --- /dev/null +++ b/apps/builder/pages/typebots/folders/[id].tsx @@ -0,0 +1,44 @@ +import withAuth from 'components/HOC/withUser' +import React from 'react' +import { Flex, Stack } from '@chakra-ui/layout' +import { DashboardHeader } from 'components/dashboard/DashboardHeader' +import { Seo } from 'components/Seo' +import { FolderContent } from 'components/dashboard/FolderContent' +import { useRouter } from 'next/router' +import { useFolderContent } from 'services/folders' +import { Spinner, useToast } from '@chakra-ui/react' + +const FolderPage = () => { + const router = useRouter() + + const toast = useToast({ + position: 'top-right', + status: 'error', + }) + + const { folder } = useFolderContent({ + folderId: router.query.id?.toString(), + onError: (error) => { + toast({ + title: "Couldn't fetch folder content", + description: error.message, + }) + }, + }) + + return ( + + + + {!folder ? ( + + + + ) : ( + + )} + + ) +} + +export default withAuth(FolderPage) diff --git a/apps/builder/pages/typebots/index.tsx b/apps/builder/pages/typebots/index.tsx deleted file mode 100644 index ca1df0135..000000000 --- a/apps/builder/pages/typebots/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import withAuth, { withAuthProps } from 'components/HOC/withUser' -import { Text } from '@chakra-ui/react' -import React from 'react' - -const TypebotsPage = ({ user }: withAuthProps) => { - return Hello {user?.email} -} - -export default withAuth(TypebotsPage) diff --git a/apps/builder/public/favicon.png b/apps/builder/public/favicon.png new file mode 100644 index 000000000..7512abc2a Binary files /dev/null and b/apps/builder/public/favicon.png differ diff --git a/apps/builder/services/api/utils.ts b/apps/builder/services/api/utils.ts new file mode 100644 index 000000000..ab872e261 --- /dev/null +++ b/apps/builder/services/api/utils.ts @@ -0,0 +1,4 @@ +import { NextApiResponse } from 'next' + +export const methodNotAllowed = (res: NextApiResponse) => + res.status(405).json({ message: 'Method Not Allowed' }) diff --git a/apps/builder/services/folders.ts b/apps/builder/services/folders.ts new file mode 100644 index 000000000..3f9135fe2 --- /dev/null +++ b/apps/builder/services/folders.ts @@ -0,0 +1,69 @@ +import { DashboardFolder } from '.prisma/client' +import useSWR from 'swr' +import { fetcher, sendRequest } from './utils' + +export const useFolders = ({ + parentId, + onError, +}: { + parentId?: string + onError: (error: Error) => void +}) => { + const params = new URLSearchParams( + parentId ? { parentId: parentId.toString() } : undefined + ) + const { data, error, mutate } = useSWR<{ folders: DashboardFolder[] }, Error>( + `/api/folders?${params}`, + fetcher + ) + if (error) onError(error) + return { + folders: data?.folders, + isLoading: !error && !data, + mutate, + } +} + +export const useFolderContent = ({ + 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, + } +} + +export const createFolder = async ( + folder: Pick +) => + sendRequest({ + url: `/api/folders`, + method: 'POST', + body: folder, + }) + +export const deleteFolder = async (id: string) => + sendRequest({ + url: `/api/folders/${id}`, + method: 'DELETE', + }) + +export const updateFolder = async ( + id: string, + folder: Partial +) => + sendRequest({ + url: `/api/folders/${id}`, + method: 'PATCH', + body: folder, + }) diff --git a/apps/builder/services/typebots.ts b/apps/builder/services/typebots.ts new file mode 100644 index 000000000..44c37e176 --- /dev/null +++ b/apps/builder/services/typebots.ts @@ -0,0 +1,69 @@ +import { Typebot } from '@typebot/prisma' +import useSWR from 'swr' +import { fetcher, sendRequest } from './utils' + +export const useTypebots = ({ + folderId, + onError, +}: { + folderId?: string + onError: (error: Error) => void +}) => { + const params = new URLSearchParams( + folderId ? { folderId: folderId.toString() } : undefined + ) + const { data, error, mutate } = useSWR<{ typebots: Typebot[] }, Error>( + `/api/typebots?${params}`, + fetcher + ) + if (error) onError(error) + return { + typebots: data?.typebots, + isLoading: !error && !data, + mutate, + } +} + +export const createTypebot = async ({ + folderId, +}: Pick) => { + const typebot = { + folderId, + name: 'My typebot', + } + return sendRequest({ + url: `/api/typebots`, + method: 'POST', + body: typebot, + }) +} + +export const duplicateTypebot = async ({ + folderId, + ownerId, + name, +}: Typebot) => { + const typebot = { + folderId, + ownerId, + name: `${name} copy`, + } + return sendRequest({ + url: `/api/typebots`, + method: 'POST', + body: typebot, + }) +} + +export const deleteTypebot = async (id: string) => + sendRequest({ + url: `/api/typebots/${id}`, + method: 'DELETE', + }) + +export const updateTypebot = async (id: string, typebot: Partial) => + sendRequest({ + url: `/api/typebots/${id}`, + method: 'PATCH', + body: typebot, + }) diff --git a/apps/builder/services/user.ts b/apps/builder/services/user.ts new file mode 100644 index 000000000..70169a32a --- /dev/null +++ b/apps/builder/services/user.ts @@ -0,0 +1,7 @@ +import { User } from '@typebot/prisma' +import { useSession } from 'next-auth/react' + +export const useUser = (): User | undefined => { + const { data } = useSession() + return data?.user as User | undefined +} diff --git a/apps/builder/services/utils.ts b/apps/builder/services/utils.ts new file mode 100644 index 000000000..02626880e --- /dev/null +++ b/apps/builder/services/utils.ts @@ -0,0 +1,32 @@ +export const fetcher = async (input: RequestInfo, init?: RequestInit) => { + const res = await fetch(input, init) + return res.json() +} + +export const isMobile = + typeof window !== 'undefined' && + window.matchMedia('only screen and (max-width: 760px)').matches + +export const sendRequest = async ({ + url, + method, + body, +}: { + url: string + method: string + body?: Record +}): Promise<{ data?: ResponseData; error?: Error }> => { + try { + const response = await fetch(url, { + method, + mode: 'cors', + body: body ? JSON.stringify(body) : undefined, + }) + if (!response.ok) throw new Error(response.statusText) + const data = await response.json() + return { data } + } catch (e) { + console.error(e) + return { error: e as Error } + } +} diff --git a/apps/builder/tsconfig.json b/apps/builder/tsconfig.json index 7b841352e..53c073685 100644 --- a/apps/builder/tsconfig.json +++ b/apps/builder/tsconfig.json @@ -4,7 +4,7 @@ "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, - "strict": false, + "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "incremental": true, diff --git a/apps/viewer/package.json b/apps/viewer/package.json index a065550fb..211c89679 100644 --- a/apps/viewer/package.json +++ b/apps/viewer/package.json @@ -2,7 +2,7 @@ "name": "viewer", "packageManager": "yarn@3.1.0", "scripts": { - "dev": "next dev", + "dev": "next dev -p 3001", "build": "next build", "start": "next start", "lint": "next lint" diff --git a/docker-compose.yml b/docker-compose.yml index 264847d10..a6b993f5c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,5 +7,8 @@ services: restart: always volumes: - db_data:/var/lib/postgresql/data + environment: + POSTGRES_PASSWORD: "" + POSTGRES_HOST_AUTH_METHOD: trust volumes: db_data: diff --git a/package.json b/package.json index 070188969..2fc7a9082 100644 --- a/package.json +++ b/package.json @@ -7,15 +7,17 @@ "apps/*" ], "scripts": { - "db:up": "docker-compose up -d", - "db:setup": "yarn workspace @typebot/prisma dev", + "db:up": "docker-compose up -d && yarn workspace @typebot/prisma prisma db push", "db:nuke": "docker-compose down --volumes --remove-orphans", - "db:inspect": "dotenv -e .env yarn workspace @typebot/prisma prisma studio", - "dev:setup": "dotenv -e .env run-s db:up db:setup", + "dev": "concurrently -n builder,viewer \"yarn dev:builder\" \"yarn dev:viewer\"", "dev:builder": "yarn workspace builder dev", - "dev:viewer": "yarn workspace viewer dev" + "dev:viewer": "yarn workspace viewer dev", + "dx": "run-s db:up dev", + "cypress:builder": "yarn workspace builder cypress", + "cypress:viewer": "yarn workspace builder cypress" }, "devDependencies": { + "concurrently": "^6.4.0", "dotenv-cli": "^4.1.0", "npm-run-all": "^4.1.5" } diff --git a/packages/prisma/package.json b/packages/prisma/package.json index bd7518289..fc9fc89b2 100644 --- a/packages/prisma/package.json +++ b/packages/prisma/package.json @@ -4,19 +4,14 @@ "devDependencies": { "dotenv-cli": "^4.1.0", "npm-run-all": "^4.1.5", - "prisma": "^3.5.0", + "prisma": "latest", "ts-node": "^10.4.0", "typescript": "^4.5.2" }, "dependencies": { - "@prisma/client": "^3.5.0" + "@prisma/client": "latest" }, "scripts": { - "prisma": "dotenv -e ../../.env prisma", - "dev": "run-s migrate generate build", - "build": "dotenv -e ../../.env tsc --build", - "migrate": "dotenv -e ../../.env prisma migrate dev", - "push": "dotenv -e ../../.env prisma db push", - "generate": "dotenv -e ../../.env prisma generate" + "prisma": "dotenv -e ../../.env prisma" } } diff --git a/packages/prisma/prisma/migrations/20211206144727_add_barebones/migration.sql b/packages/prisma/prisma/migrations/20211206144727_add_barebones/migration.sql new file mode 100644 index 000000000..b937a6fb5 --- /dev/null +++ b/packages/prisma/prisma/migrations/20211206144727_add_barebones/migration.sql @@ -0,0 +1,70 @@ +-- CreateEnum +CREATE TYPE "Plan" AS ENUM ('FREE', 'PRO'); + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "plan" "Plan" NOT NULL DEFAULT E'FREE'; + +-- CreateTable +CREATE TABLE "DashboardFolder" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "name" TEXT NOT NULL, + "ownerId" TEXT NOT NULL, + "parentFolderId" TEXT, + + CONSTRAINT "DashboardFolder_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Typebot" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "name" TEXT NOT NULL, + "ownerId" TEXT NOT NULL, + "publishedTypebotId" TEXT, + "folderId" TEXT, + + CONSTRAINT "Typebot_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PublicTypebot" ( + "id" TEXT NOT NULL, + "typebotId" TEXT NOT NULL, + "steps" JSONB[], + "name" TEXT NOT NULL, + + CONSTRAINT "PublicTypebot_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Result" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "typebotId" TEXT NOT NULL, + + CONSTRAINT "Result_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "PublicTypebot_typebotId_key" ON "PublicTypebot"("typebotId"); + +-- AddForeignKey +ALTER TABLE "DashboardFolder" ADD CONSTRAINT "DashboardFolder_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DashboardFolder" ADD CONSTRAINT "DashboardFolder_parentFolderId_fkey" FOREIGN KEY ("parentFolderId") REFERENCES "DashboardFolder"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Typebot" ADD CONSTRAINT "Typebot_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Typebot" ADD CONSTRAINT "Typebot_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "DashboardFolder"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PublicTypebot" ADD CONSTRAINT "PublicTypebot_typebotId_fkey" FOREIGN KEY ("typebotId") REFERENCES "Typebot"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Result" ADD CONSTRAINT "Result_typebotId_fkey" FOREIGN KEY ("typebotId") REFERENCES "Typebot"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/prisma/schema.draft.prisma b/packages/prisma/prisma/schema.draft.prisma new file mode 100644 index 000000000..d95c6a640 --- /dev/null +++ b/packages/prisma/prisma/schema.draft.prisma @@ -0,0 +1,87 @@ +datasource db { + url = env("DATABASE_URL") + provider = "postgresql" +} + +generator client { + provider = "prisma-client-js" +} + +model User { + id String @id + createdAt DateTime @default(now()) + email String @unique + name String? + avatarUrl String? + redeemedCoupon Boolean? + oAuthCredentials Json? + referralId String? + domains String[] + onboarding_data Json? + settings Json + typebots Typebot[] @relation("Owner") + sharedTypebots Typebot[] @relation("Collaborators") + dashboardFolders DashboardFolder[] +} + +model DashboardFolder { + id BigInt @id @default(autoincrement()) + createdAt DateTime @default(now()) + name String + owner User @relation(fields: [ownerId], references: [id]) + ownerId String + parentFolderId BigInt + parentFolder DashboardFolder @relation("ParentChild", fields: [parentFolderId], references: [id]) + childrenFolder DashboardFolder[] @relation("ParentChild") +} + +model Typebot { + id BigInt @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) + steps Json[] + publishedTypebotId BigInt @unique + publishedTypebot PublicTypebot @relation(fields: [publishedTypebotId], references: [id]) + connectors Json[] + name String + ownerId String + owner User @relation("Owner", fields: [ownerId], references: [id]) + conditions Json + startConditions Json + theme Json + settings Json + collaborators User[] @relation("Collaborators") + customDomains String[] + shareSettings Json + variables Json + checkedConversionRules String[] + results Result[] + httpRequests Json[] + credentials Json[] +} + +model PublicTypebot { + id BigInt @id @default(autoincrement()) + typebot Typebot? + steps Json[] + name String + conditions Json + startConditions Json + theme Json + settings Json + connectors Json + customDomains String[] + shareSettings Json + variables Json +} + +model Result { + id BigInt @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) + typebotId BigInt + typebot Typebot @relation(fields: [typebotId], references: [id]) + variables Json[] + isCompleted Boolean + answers Json[] +} diff --git a/packages/prisma/prisma/schema.prisma b/packages/prisma/prisma/schema.prisma index c1c7f97c5..403195070 100644 --- a/packages/prisma/prisma/schema.prisma +++ b/packages/prisma/prisma/schema.prisma @@ -4,24 +4,24 @@ datasource db { } generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" } model Account { - id String @id @default(cuid()) - userId String - type String - provider String - providerAccountId String - refresh_token String? - access_token String? - expires_at Int? - token_type String? - scope String? - id_token String? - session_state String? - oauth_token_secret String? - oauth_token String? + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? + access_token String? + expires_at Int? + token_type String? + scope String? + id_token String? + session_state String? + oauth_token_secret String? + oauth_token String? refresh_token_expires_in Int? user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@ -38,13 +38,21 @@ model Session { } model User { - id String @id @default(cuid()) + id String @id @default(cuid()) name String? - email String? @unique + email String? @unique emailVerified DateTime? image String? accounts Account[] sessions Session[] + typebots Typebot[] + folders DashboardFolder[] + plan Plan @default(FREE) +} + +enum Plan { + FREE + PRO } model VerificationToken { @@ -53,4 +61,46 @@ model VerificationToken { expires DateTime @@unique([identifier, token]) -} \ No newline at end of file +} + +model DashboardFolder { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + name String + owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) + ownerId String + parentFolderId String? + parentFolder DashboardFolder? @relation("ParentChild", fields: [parentFolderId], references: [id]) + childrenFolder DashboardFolder[] @relation("ParentChild") + typebots Typebot[] +} + +model Typebot { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) + name String + ownerId String + owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) + publishedTypebotId String? + publishedTypebot PublicTypebot? + results Result[] + folderId String? + folder DashboardFolder? @relation(fields: [folderId], references: [id]) +} + +model PublicTypebot { + id String @id @default(cuid()) + typebotId String @unique + typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade) + steps Json[] + name String +} + +model Result { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) + typebotId String + typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade) +} diff --git a/yarn.lock b/yarn.lock index 7e6b68a3e..7079e8abc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -287,7 +287,7 @@ __metadata: languageName: node linkType: hard -"@chakra-ui/css-reset@npm:1.1.1": +"@chakra-ui/css-reset@npm:1.1.1, @chakra-ui/css-reset@npm:^1.1.1": version: 1.1.1 resolution: "@chakra-ui/css-reset@npm:1.1.1" peerDependencies: @@ -1010,6 +1010,42 @@ __metadata: languageName: node linkType: hard +"@dnd-kit/accessibility@npm:^3.0.0": + version: 3.0.0 + resolution: "@dnd-kit/accessibility@npm:3.0.0" + dependencies: + tslib: ^2.0.0 + peerDependencies: + react: ">=16.8.0" + checksum: feac6afea07ece1568df38973d7a1a1b994782a6df119983250c3849ca2a3818a7bcc67fe4c5d22b0a1cfde866081644d677d2188abed507b3eccc255da79320 + languageName: node + linkType: hard + +"@dnd-kit/core@npm:^4.0.3": + version: 4.0.3 + resolution: "@dnd-kit/core@npm:4.0.3" + dependencies: + "@dnd-kit/accessibility": ^3.0.0 + "@dnd-kit/utilities": ^3.0.1 + tslib: ^2.0.0 + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 828609317504f32bf37238840e5d2ae545efaaa779d696fdad1606d2ae22ae5668a43466db007efde89c8c1ef04ca66b2c61b6e98465f9487ec37c8e3d11a1b2 + languageName: node + linkType: hard + +"@dnd-kit/utilities@npm:^3.0.1": + version: 3.0.1 + resolution: "@dnd-kit/utilities@npm:3.0.1" + dependencies: + tslib: ^2.0.0 + peerDependencies: + react: ">=16.8.0" + checksum: 01ffe77bf98707e2e0e949b4425caf6a237ad4fc07fe15f9f0e4ab7c710d81ba2190915549c5302386cb55517e7be4bea6560dfb7eba0a516fa46719f837ec57 + languageName: node + linkType: hard + "@emotion/babel-plugin@npm:^11.3.0": version: 11.3.0 resolution: "@emotion/babel-plugin@npm:11.3.0" @@ -1529,31 +1565,31 @@ __metadata: languageName: node linkType: hard -"@prisma/client@npm:^3.5.0": - version: 3.5.0 - resolution: "@prisma/client@npm:3.5.0" +"@prisma/client@npm:latest": + version: 3.6.0 + resolution: "@prisma/client@npm:3.6.0" dependencies: - "@prisma/engines-version": 3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e + "@prisma/engines-version": 3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727 peerDependencies: prisma: "*" peerDependenciesMeta: prisma: optional: true - checksum: 515880b22c4df43185cefb17fe34968c9ba51cf4fead39e2a9bd9da7ba58a3303194ca0d81fcee27e9a14caa017d3f92cffdcdb4e90fa912ba1ca9508991503d + checksum: 83835117d60d100cf0933f60933603f5b85833414ad4dae9ea6b18f99190030317de459bbb573e928f764b476733676722fd90637292defae887dd974a2914a1 languageName: node linkType: hard -"@prisma/engines-version@npm:3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e": - version: 3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e - resolution: "@prisma/engines-version@npm:3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e" - checksum: 2919f44abc369ec48c8b2c0169293934ba7e2cff6e2bf29a86731b9e1a8c6bad8a3ffc41e604187ea75cfbb04b138bad7e4fe8ec7b557b437e0e2264ef9806df +"@prisma/engines-version@npm:3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727": + version: 3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727 + resolution: "@prisma/engines-version@npm:3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727" + checksum: 40adea282d62b7d8d03696f3f9f1c6da271671955f5305d3750d07f80eff7bcfad89430e97215183d027895487c4ab36373fc8f59df1a537eb2ab18458168993 languageName: node linkType: hard -"@prisma/engines@npm:3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e": - version: 3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e - resolution: "@prisma/engines@npm:3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e" - checksum: 2a31da94fef3c497d66ae77174c1a6627e7aae63039b01cbd4f2dbc4b44198dd4932af48a013566bb54ed173dd948d75b8d47bfb4f139551cc685f2e2cf2f0ad +"@prisma/engines@npm:3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727": + version: 3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727 + resolution: "@prisma/engines@npm:3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727" + checksum: 3eb1e40dbd6ca8e8bd727048393b9fd993b415bcc1efca2f6bac5b0a60da8e73c2dd35602c4083b29fc31fd17a2cc9ba0522afe1b6499a68663ecdf5f833cdb3 languageName: node linkType: hard @@ -1689,10 +1725,10 @@ __metadata: version: 0.0.0-use.local resolution: "@typebot/prisma@workspace:packages/prisma" dependencies: - "@prisma/client": ^3.5.0 + "@prisma/client": latest dotenv-cli: ^4.1.0 npm-run-all: ^4.1.5 - prisma: ^3.5.0 + prisma: latest ts-node: ^10.4.0 typescript: ^4.5.2 languageName: unknown @@ -2663,7 +2699,9 @@ __metadata: version: 0.0.0-use.local resolution: "builder@workspace:apps/builder" dependencies: + "@chakra-ui/css-reset": ^1.1.1 "@chakra-ui/react": ^1.7.2 + "@dnd-kit/core": ^4.0.3 "@emotion/react": ^11 "@emotion/styled": ^11 "@next-auth/prisma-adapter": next @@ -2681,6 +2719,7 @@ __metadata: eslint-config-prettier: ^8.3.0 eslint-plugin-cypress: ^2.12.1 eslint-plugin-prettier: ^4.0.0 + focus-visible: ^5.2.0 framer-motion: ^4 next: ^12.0.4 next-auth: beta @@ -2689,7 +2728,9 @@ __metadata: prettier: ^2.4.1 react: ^17.0.2 react-dom: ^17.0.2 + swr: ^1.0.1 typescript: ^4.5.2 + use-debounce: ^7.0.1 languageName: unknown linkType: soft @@ -2899,6 +2940,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^7.0.2": + version: 7.0.4 + resolution: "cliui@npm:7.0.4" + dependencies: + string-width: ^4.2.0 + strip-ansi: ^6.0.0 + wrap-ansi: ^7.0.0 + checksum: ce2e8f578a4813806788ac399b9e866297740eecd4ad1823c27fd344d78b22c5f8597d548adbcc46f0573e43e21e751f39446c5a5e804a12aace402b7a315d7f + languageName: node + linkType: hard + "code-point-at@npm:^1.0.0": version: 1.1.0 resolution: "code-point-at@npm:1.1.0" @@ -3015,6 +3067,24 @@ __metadata: languageName: node linkType: hard +"concurrently@npm:^6.4.0": + version: 6.4.0 + resolution: "concurrently@npm:6.4.0" + dependencies: + chalk: ^4.1.0 + date-fns: ^2.16.1 + lodash: ^4.17.21 + rxjs: ^6.6.3 + spawn-command: ^0.0.2-1 + supports-color: ^8.1.0 + tree-kill: ^1.2.2 + yargs: ^16.2.0 + bin: + concurrently: bin/concurrently.js + checksum: 902864cc853176cac406246fa367a1b24ebfcbea9ba43c164f9cb4e2c9c1a5f9d8be05ce98fe7ef13329ffd5cc340a052007a9c870863e2068425d95b3bf89d7 + languageName: node + linkType: hard + "console-control-strings@npm:^1.0.0, console-control-strings@npm:~1.1.0": version: 1.1.0 resolution: "console-control-strings@npm:1.1.0" @@ -3316,6 +3386,13 @@ __metadata: languageName: node linkType: hard +"date-fns@npm:^2.16.1": + version: 2.27.0 + resolution: "date-fns@npm:2.27.0" + checksum: db62036b3816eb0322c9532b353beac7f660a91e1a55dbd21c14893c621ebb8509f21c66ba287844dabd34dee0207edd54a9537bce6bb7cab9383dedc6b8bc90 + languageName: node + linkType: hard + "dayjs@npm:^1.10.4": version: 1.10.7 resolution: "dayjs@npm:1.10.7" @@ -3390,6 +3467,13 @@ __metadata: languageName: node linkType: hard +"dequal@npm:2.0.2": + version: 2.0.2 + resolution: "dequal@npm:2.0.2" + checksum: 86c7a2c59f7b0797ed397c74b5fcdb744e48fc19440b70ad6ac59f57550a96b0faef3f1cfd5760ec5e6d3f7cb101f634f1f80db4e727b1dc8389bf62d977c0a0 + languageName: node + linkType: hard + "des.js@npm:^1.0.0": version: 1.0.1 resolution: "des.js@npm:1.0.1" @@ -4260,6 +4344,13 @@ __metadata: languageName: node linkType: hard +"focus-visible@npm:^5.2.0": + version: 5.2.0 + resolution: "focus-visible@npm:5.2.0" + checksum: 876f646ef453680d3d34e9f9b23961527ffd5ccaed8690f423d4fbfa37ff023d98a490972bc1387850e37ec2e44958c81f6096ef95b67462e5c4b5404cf1dbb9 + languageName: node + linkType: hard + "foreach@npm:^2.0.5": version: 2.0.5 resolution: "foreach@npm:2.0.5" @@ -4391,6 +4482,13 @@ __metadata: languageName: node linkType: hard +"get-caller-file@npm:^2.0.5": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 + languageName: node + linkType: hard + "get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.0, get-intrinsic@npm:^1.1.1": version: 1.1.1 resolution: "get-intrinsic@npm:1.1.1" @@ -6618,15 +6716,15 @@ __metadata: languageName: node linkType: hard -"prisma@npm:^3.5.0": - version: 3.5.0 - resolution: "prisma@npm:3.5.0" +"prisma@npm:latest": + version: 3.6.0 + resolution: "prisma@npm:3.6.0" dependencies: - "@prisma/engines": 3.5.0-38.78a5df6def6943431f4c022e1428dbc3e833cf8e + "@prisma/engines": 3.6.0-24.dc520b92b1ebb2d28dc3161f9f82e875bd35d727 bin: prisma: build/index.js prisma2: build/index.js - checksum: 5ac2558dc29a8c325d991e59de2208bf6756bddb4fa2d3d97cd17aa9daa94611515d3ab284777adcda2c7b918b0624316e7081bae5d1c01b449738b8ebf81882 + checksum: 365718b1e0ed8491aedb79bda39c500e8fddae0d62f57ed0ab3786c6709bd79d43b61c3603847b165b5a3de1e983b64388902a9ed55d4d5cb5878d67af23da8b languageName: node linkType: hard @@ -7040,6 +7138,13 @@ __metadata: languageName: node linkType: hard +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: fb47e70bf0001fdeabdc0429d431863e9475e7e43ea5f94ad86503d918423c1543361cc5166d713eaa7029dd7a3d34775af04764bebff99ef413111a5af18c80 + languageName: node + linkType: hard + "require-from-string@npm:^2.0.2": version: 2.0.2 resolution: "require-from-string@npm:2.0.2" @@ -7166,6 +7271,15 @@ __metadata: languageName: node linkType: hard +"rxjs@npm:^6.6.3": + version: 6.6.7 + resolution: "rxjs@npm:6.6.7" + dependencies: + tslib: ^1.9.0 + checksum: bc334edef1bb8bbf56590b0b25734ba0deaf8825b703256a93714308ea36dff8a11d25533671adf8e104e5e8f256aa6fdfe39b2e248cdbd7a5f90c260acbbd1b + languageName: node + linkType: hard + "rxjs@npm:^7.4.0": version: 7.4.0 resolution: "rxjs@npm:7.4.0" @@ -7412,6 +7526,13 @@ __metadata: languageName: node linkType: hard +"spawn-command@npm:^0.0.2-1": + version: 0.0.2 + resolution: "spawn-command@npm:0.0.2" + checksum: e35c5d28177b4d461d33c88cc11f6f3a5079e2b132c11e1746453bbb7a0c0b8a634f07541a2a234fa4758239d88203b758def509161b651e81958894c0b4b64b + languageName: node + linkType: hard + "spdx-correct@npm:^3.0.0": version: 3.1.1 resolution: "spdx-correct@npm:3.1.1" @@ -7735,7 +7856,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^8.0.0, supports-color@npm:^8.1.1": +"supports-color@npm:^8.0.0, supports-color@npm:^8.1.0, supports-color@npm:^8.1.1": version: 8.1.1 resolution: "supports-color@npm:8.1.1" dependencies: @@ -7744,6 +7865,17 @@ __metadata: languageName: node linkType: hard +"swr@npm:^1.0.1": + version: 1.0.1 + resolution: "swr@npm:1.0.1" + dependencies: + dequal: 2.0.2 + peerDependencies: + react: ^16.11.0 || ^17.0.0 + checksum: 8aaa10c4c65cb9b46a143a52ac2728111fc8af96e83781df1f7b7d56aa027ef720b7feb230658616e479f224f684d4cbc5d2ca3265c40f95a3140dbdba801061 + languageName: node + linkType: hard + "table@npm:^6.0.9": version: 6.7.3 resolution: "table@npm:6.7.3" @@ -7873,6 +8005,15 @@ __metadata: languageName: node linkType: hard +"tree-kill@npm:^1.2.2": + version: 1.2.2 + resolution: "tree-kill@npm:1.2.2" + bin: + tree-kill: cli.js + checksum: 49117f5f410d19c84b0464d29afb9642c863bc5ba40fcb9a245d474c6d5cc64d1b177a6e6713129eb346b40aebb9d4631d967517f9fbe8251c35b21b13cd96c7 + languageName: node + linkType: hard + "ts-node@npm:^10.4.0": version: 10.4.0 resolution: "ts-node@npm:10.4.0" @@ -7921,14 +8062,14 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.0.0, tslib@npm:^1.8.1, tslib@npm:^1.9.3": +"tslib@npm:^1.0.0, tslib@npm:^1.8.1, tslib@npm:^1.9.0, tslib@npm:^1.9.3": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: dbe628ef87f66691d5d2959b3e41b9ca0045c3ee3c7c7b906cc1e328b39f199bb1ad9e671c39025bd56122ac57dfbf7385a94843b1cc07c60a4db74795829acd languageName: node linkType: hard -"tslib@npm:^2.0.3, tslib@npm:^2.1.0": +"tslib@npm:^2.0.0, tslib@npm:^2.0.3, tslib@npm:^2.1.0": version: 2.3.1 resolution: "tslib@npm:2.3.1" checksum: de17a98d4614481f7fcb5cd53ffc1aaf8654313be0291e1bfaee4b4bb31a20494b7d218ff2e15017883e8ea9626599b3b0e0229c18383ba9dce89da2adf15cb9 @@ -8010,6 +8151,7 @@ __metadata: version: 0.0.0-use.local resolution: "typebot-os@workspace:." dependencies: + concurrently: ^6.4.0 dotenv-cli: ^4.1.0 npm-run-all: ^4.1.5 languageName: unknown @@ -8125,6 +8267,15 @@ __metadata: languageName: node linkType: hard +"use-debounce@npm:^7.0.1": + version: 7.0.1 + resolution: "use-debounce@npm:7.0.1" + peerDependencies: + react: ">=16.8.0" + checksum: c5c1c34ccef6c11fe25fecef983a07f5a7260fce77b58f0f648f6ae08693a22a9077b5362b96154896e9ea756e6aaea303eb44550a293456cecb6f15cf87d73b + languageName: node + linkType: hard + "use-sidecar@npm:^1.0.1, use-sidecar@npm:^1.0.5": version: 1.0.5 resolution: "use-sidecar@npm:1.0.5" @@ -8380,6 +8531,13 @@ __metadata: languageName: node linkType: hard +"y18n@npm:^5.0.5": + version: 5.0.8 + resolution: "y18n@npm:5.0.8" + checksum: 54f0fb95621ee60898a38c572c515659e51cc9d9f787fb109cef6fde4befbe1c4602dc999d30110feee37456ad0f1660fa2edcfde6a9a740f86a290999550d30 + languageName: node + linkType: hard + "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" @@ -8394,6 +8552,28 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^20.2.2": + version: 20.2.9 + resolution: "yargs-parser@npm:20.2.9" + checksum: 8bb69015f2b0ff9e17b2c8e6bfe224ab463dd00ca211eece72a4cd8a906224d2703fb8a326d36fdd0e68701e201b2a60ed7cf81ce0fd9b3799f9fe7745977ae3 + languageName: node + linkType: hard + +"yargs@npm:^16.2.0": + version: 16.2.0 + resolution: "yargs@npm:16.2.0" + dependencies: + cliui: ^7.0.2 + escalade: ^3.1.1 + get-caller-file: ^2.0.5 + require-directory: ^2.1.1 + string-width: ^4.2.0 + y18n: ^5.0.5 + yargs-parser: ^20.2.2 + checksum: b14afbb51e3251a204d81937c86a7e9d4bdbf9a2bcee38226c900d00f522969ab675703bee2a6f99f8e20103f608382936034e64d921b74df82b63c07c5e8f59 + languageName: node + linkType: hard + "yauzl@npm:^2.10.0": version: 2.10.0 resolution: "yauzl@npm:2.10.0"