2
0

♻️ (builder) Change to features-centric folder structure

This commit is contained in:
Baptiste Arnaud
2022-11-15 09:35:48 +01:00
committed by Baptiste Arnaud
parent 3686465a85
commit 643571fe7d
683 changed files with 3907 additions and 3643 deletions

View File

@ -0,0 +1,45 @@
import {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useContext,
useEffect,
useState,
} from 'react'
import { TypebotInDashboard } from '@/features/dashboard'
const typebotDndContext = createContext<{
draggedTypebot?: TypebotInDashboard
setDraggedTypebot: Dispatch<SetStateAction<TypebotInDashboard | undefined>>
mouseOverFolderId?: string | null
setMouseOverFolderId: Dispatch<SetStateAction<string | undefined | null>>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
}>({})
export const TypebotDndProvider = ({ children }: { children: ReactNode }) => {
const [draggedTypebot, setDraggedTypebot] = useState<TypebotInDashboard>()
const [mouseOverFolderId, setMouseOverFolderId] = useState<string | null>()
useEffect(() => {
draggedTypebot
? document.body.classList.add('grabbing')
: document.body.classList.remove('grabbing')
}, [draggedTypebot])
return (
<typebotDndContext.Provider
value={{
draggedTypebot,
setDraggedTypebot,
mouseOverFolderId,
setMouseOverFolderId,
}}
>
{children}
</typebotDndContext.Provider>
)
}
export const useTypebotDnd = () => useContext(typebotDndContext)

View File

@ -0,0 +1,32 @@
import { Button } from '@chakra-ui/react'
import { ChevronLeftIcon } from '@/components/icons'
import { useTypebotDnd } from '../TypebotDndProvider'
import Link from 'next/link'
import React, { useMemo } from 'react'
export const BackButton = ({ id }: { id: string | null }) => {
const { draggedTypebot, setMouseOverFolderId, mouseOverFolderId } =
useTypebotDnd()
const isTypebotOver = useMemo(
() => draggedTypebot && mouseOverFolderId === id,
[draggedTypebot, id, mouseOverFolderId]
)
const handleMouseEnter = () => setMouseOverFolderId(id)
const handleMouseLeave = () => setMouseOverFolderId(undefined)
return (
<Button
as={Link}
href={id ? `/typebots/folders/${id}` : '/typebots'}
leftIcon={<ChevronLeftIcon />}
variant={'outline'}
colorScheme={isTypebotOver ? 'blue' : 'gray'}
borderWidth={isTypebotOver ? '3px' : '1px'}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
Back
</Button>
)
}

View File

@ -0,0 +1,47 @@
import { Button, ButtonProps, Text, VStack } from '@chakra-ui/react'
import { PlusIcon } from '@/components/icons'
import { useRouter } from 'next/router'
import { stringify } from 'qs'
import React from 'react'
export const CreateBotButton = ({
folderId,
isFirstBot,
...props
}: { folderId?: string; isFirstBot: boolean } & ButtonProps) => {
const router = useRouter()
const handleClick = () =>
router.push(
`/typebots/create?${stringify({
isFirstBot: !isFirstBot ? undefined : isFirstBot,
folderId,
})}`
)
return (
<Button
mr={{ sm: 6 }}
mb={6}
style={{ width: '225px', height: '270px' }}
onClick={handleClick}
paddingX={6}
whiteSpace={'normal'}
colorScheme="blue"
{...props}
>
<VStack spacing="6">
<PlusIcon fontSize="40px" />
<Text
fontSize={18}
fontWeight="medium"
maxW={40}
textAlign="center"
mt="6"
>
Create a typebot
</Text>
</VStack>
</Button>
)
}

View File

@ -0,0 +1,40 @@
import { Button, HStack, useDisclosure, Text } from '@chakra-ui/react'
import { FolderPlusIcon } from '@/components/icons'
import {
LimitReached,
ChangePlanModal,
LockTag,
isFreePlan,
} from '@/features/billing'
import { useWorkspace } from '@/features/workspace'
import { Plan } from 'db'
import React from 'react'
type Props = { isLoading: boolean; onClick: () => void }
export const CreateFolderButton = ({ isLoading, onClick }: Props) => {
const { workspace } = useWorkspace()
const { isOpen, onOpen, onClose } = useDisclosure()
const handleClick = () => {
if (isFreePlan(workspace)) return onOpen()
onClick()
}
return (
<Button
leftIcon={<FolderPlusIcon />}
onClick={handleClick}
isLoading={isLoading}
>
<HStack>
<Text>Create a folder</Text>
{isFreePlan(workspace) && <LockTag plan={Plan.STARTER} />}
</HStack>
<ChangePlanModal
isOpen={isOpen}
onClose={onClose}
type={LimitReached.FOLDER}
/>
</Button>
)
}

View File

@ -0,0 +1,164 @@
import { DashboardFolder } from 'db'
import {
Button,
Editable,
EditableInput,
EditablePreview,
MenuItem,
useDisclosure,
Text,
VStack,
IconButton,
Menu,
MenuButton,
MenuList,
SkeletonText,
SkeletonCircle,
WrapItem,
} from '@chakra-ui/react'
import { FolderIcon, MoreVerticalIcon } from '@/components/icons'
import { ConfirmModal } from '@/components/ConfirmModal'
import { useTypebotDnd } from '../TypebotDndProvider'
import { useRouter } from 'next/router'
import React, { useMemo } from 'react'
import { deleteFolderQuery } from '../queries/deleteFolderQuery'
import { useToast } from '@/hooks/useToast'
import { updateFolderQuery } from '../queries/updateFolderQuery'
export const FolderButton = ({
folder,
onFolderDeleted,
onFolderRenamed,
}: {
folder: DashboardFolder
onFolderDeleted: () => void
onFolderRenamed: (newName: string) => void
}) => {
const router = useRouter()
const { draggedTypebot, setMouseOverFolderId, mouseOverFolderId } =
useTypebotDnd()
const isTypebotOver = useMemo(
() => draggedTypebot && mouseOverFolderId === folder.id,
[draggedTypebot, folder.id, mouseOverFolderId]
)
const { isOpen, onOpen, onClose } = useDisclosure()
const { showToast } = useToast()
const onDeleteClick = async () => {
const { error } = await deleteFolderQuery(folder.id)
return error
? showToast({
title: "Couldn't delete the folder",
description: error.message,
})
: onFolderDeleted()
}
const onRenameSubmit = async (newName: string) => {
if (newName === '' || newName === folder.name) return
const { error } = await updateFolderQuery(folder.id, { name: newName })
return error
? showToast({ title: 'An error occured', description: error.message })
: onFolderRenamed(newName)
}
const handleClick = () => {
router.push(`/typebots/folders/${folder.id}`)
}
const handleMouseEnter = () => setMouseOverFolderId(folder.id)
const handleMouseLeave = () => setMouseOverFolderId(undefined)
return (
<Button
as={WrapItem}
style={{ width: '225px', height: '270px' }}
paddingX={6}
whiteSpace={'normal'}
pos="relative"
cursor="pointer"
variant="outline"
colorScheme={isTypebotOver ? 'blue' : 'gray'}
borderWidth={isTypebotOver ? '3px' : '1px'}
justifyContent="center"
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<Menu>
<MenuButton
as={IconButton}
icon={<MoreVerticalIcon />}
aria-label={`Show ${folder.name} menu`}
onClick={(e) => e.stopPropagation()}
colorScheme="gray"
variant="outline"
size="sm"
pos="absolute"
top="5"
right="5"
/>
<MenuList>
<MenuItem
color="red"
onClick={(e) => {
e.stopPropagation()
onOpen()
}}
>
Delete
</MenuItem>
</MenuList>
</Menu>
<VStack spacing="4">
<FolderIcon fontSize={50} color="blue.500" fill="blue.500" />
<Editable
defaultValue={folder.name}
fontSize="18"
onClick={(e) => e.stopPropagation()}
onSubmit={onRenameSubmit}
>
<EditablePreview
_hover={{ bgColor: 'gray.300' }}
px="2"
textAlign="center"
/>
<EditableInput textAlign="center" />
</Editable>
</VStack>
<ConfirmModal
isOpen={isOpen}
onClose={onClose}
confirmButtonLabel={'Delete'}
message={
<Text>
Are you sure you want to delete <strong>{folder.name}</strong>{' '}
folder? (Everything inside will be move to your dashboard)
</Text>
}
title={`Delete ${folder.name}?`}
onConfirm={onDeleteClick}
confirmButtonColor="red"
/>
</Button>
)
}
export const ButtonSkeleton = () => (
<Button
as={VStack}
mr={{ sm: 6 }}
mb={6}
style={{ width: '225px', height: '270px' }}
paddingX={6}
whiteSpace={'normal'}
pos="relative"
cursor="pointer"
variant="outline"
>
<VStack spacing="6" w="full">
<SkeletonCircle boxSize="45px" />
<SkeletonText noOfLines={2} w="full" />
</VStack>
</Button>
)

View File

@ -0,0 +1,232 @@
import { DashboardFolder, WorkspaceRole } from 'db'
import { env } from 'utils'
import {
Flex,
Heading,
HStack,
Portal,
Skeleton,
Stack,
useEventListener,
Wrap,
} from '@chakra-ui/react'
import { useTypebotDnd } from '../TypebotDndProvider'
import { useUser } from '@/features/account'
import React, { useState } from 'react'
import { BackButton } from './BackButton'
import { OnboardingModal } from '../../dashboard/components/OnboardingModal'
import { useWorkspace } from '@/features/workspace'
import { useToast } from '@/hooks/useToast'
import { TypebotInDashboard, useTypebots } from '@/features/dashboard'
import { useFolders } from '../hooks/useFolders'
import { patchTypebotQuery } from '../queries/patchTypebotQuery'
import { createFolderQuery } from '../queries/createFolderQuery'
import { CreateBotButton } from './CreateBotButton'
import { CreateFolderButton } from './CreateFolderButton'
import { ButtonSkeleton, FolderButton } from './FolderButton'
import { TypebotButton } from './TypebotButton'
import { TypebotCardOverlay } from './TypebotButtonOverlay'
type Props = { folder: DashboardFolder | null }
const dragDistanceTolerance = 20
export const FolderContent = ({ folder }: Props) => {
const { user } = useUser()
const { workspace, currentRole } = useWorkspace()
const [isCreatingFolder, setIsCreatingFolder] = useState(false)
const {
setDraggedTypebot,
draggedTypebot,
mouseOverFolderId,
setMouseOverFolderId,
} = useTypebotDnd()
const [mouseDownPosition, setMouseDownPosition] = useState({ x: 0, y: 0 })
const [draggablePosition, setDraggablePosition] = useState({ x: 0, y: 0 })
const [relativeDraggablePosition, setRelativeDraggablePosition] = useState({
x: 0,
y: 0,
})
const [typebotDragCandidate, setTypebotDragCandidate] =
useState<TypebotInDashboard>()
const { showToast } = useToast()
const {
folders,
isLoading: isFolderLoading,
mutate: mutateFolders,
} = useFolders({
workspaceId: workspace?.id,
parentId: folder?.id,
onError: (error) => {
showToast({ title: "Couldn't fetch folders", description: error.message })
},
})
const {
typebots,
isLoading: isTypebotLoading,
mutate: mutateTypebots,
} = useTypebots({
workspaceId: workspace?.id,
folderId: folder?.id,
onError: (error) => {
showToast({
title: "Couldn't fetch typebots",
description: error.message,
})
},
})
const moveTypebotToFolder = async (typebotId: string, folderId: string) => {
if (!typebots) return
const { error } = await patchTypebotQuery(typebotId, {
folderId: folderId === 'root' ? null : folderId,
})
if (error) showToast({ description: error.message })
mutateTypebots({ typebots: typebots.filter((t) => t.id !== typebotId) })
}
const handleCreateFolder = async () => {
if (!folders || !workspace) return
setIsCreatingFolder(true)
const { error, data: newFolder } = await createFolderQuery(workspace.id, {
parentFolderId: folder?.id ?? null,
})
setIsCreatingFolder(false)
if (error)
return showToast({
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)),
})
}
const handleMouseUp = async () => {
if (mouseOverFolderId !== undefined && draggedTypebot)
await moveTypebotToFolder(draggedTypebot.id, mouseOverFolderId ?? 'root')
setTypebotDragCandidate(undefined)
setMouseOverFolderId(undefined)
setDraggedTypebot(undefined)
}
useEventListener('mouseup', handleMouseUp)
const handleMouseDown =
(typebot: TypebotInDashboard) => (e: React.MouseEvent) => {
const element = e.currentTarget as HTMLDivElement
const rect = element.getBoundingClientRect()
setDraggablePosition({ x: rect.left, y: rect.top })
const x = e.clientX - rect.left
const y = e.clientY - rect.top
setRelativeDraggablePosition({ x, y })
setMouseDownPosition({ x: e.screenX, y: e.screenY })
setTypebotDragCandidate(typebot)
}
const handleMouseMove = (e: MouseEvent) => {
if (!typebotDragCandidate) return
const { clientX, clientY, screenX, screenY } = e
if (
Math.abs(mouseDownPosition.x - screenX) > dragDistanceTolerance ||
Math.abs(mouseDownPosition.y - screenY) > dragDistanceTolerance
)
setDraggedTypebot(typebotDragCandidate)
setDraggablePosition({
...draggablePosition,
x: clientX - relativeDraggablePosition.x,
y: clientY - relativeDraggablePosition.y,
})
}
useEventListener('mousemove', handleMouseMove)
return (
<Flex w="full" flex="1" justify="center">
{typebots &&
!isTypebotLoading &&
user &&
folder === null &&
env('E2E_TEST') !== 'true' && (
<OnboardingModal totalTypebots={typebots.length} />
)}
<Stack w="1000px" spacing={6}>
<Skeleton isLoaded={folder?.name !== undefined}>
<Heading as="h1">{folder?.name}</Heading>
</Skeleton>
<Stack>
<HStack>
{folder && <BackButton id={folder.parentFolderId} />}
{currentRole !== WorkspaceRole.GUEST && (
<CreateFolderButton
onClick={handleCreateFolder}
isLoading={isCreatingFolder || isFolderLoading}
/>
)}
</HStack>
<Wrap spacing={4}>
{currentRole !== WorkspaceRole.GUEST && (
<CreateBotButton
folderId={folder?.id}
isLoading={isTypebotLoading}
isFirstBot={typebots?.length === 0 && folder === null}
/>
)}
{isFolderLoading && <ButtonSkeleton />}
{folders &&
folders.map((folder) => (
<FolderButton
key={folder.id.toString()}
folder={folder}
onFolderDeleted={() => handleFolderDeleted(folder.id)}
onFolderRenamed={(newName: string) =>
handleFolderRenamed(folder.id, newName)
}
/>
))}
{isTypebotLoading && <ButtonSkeleton />}
{typebots &&
typebots.map((typebot) => (
<TypebotButton
key={typebot.id.toString()}
typebot={typebot}
onTypebotDeleted={() => handleTypebotDeleted(typebot.id)}
onMouseDown={handleMouseDown(typebot)}
/>
))}
</Wrap>
</Stack>
</Stack>
{draggedTypebot && (
<Portal>
<TypebotCardOverlay
typebot={draggedTypebot}
onMouseUp={handleMouseUp}
pos="fixed"
top="0"
left="0"
style={{
transform: `translate(${draggablePosition.x}px, ${draggablePosition.y}px) rotate(-2deg)`,
}}
/>
</Portal>
)}
</Flex>
)
}

View File

@ -0,0 +1,40 @@
import { Seo } from '@/components/Seo'
import { DashboardHeader } from '@/features/dashboard'
import { useToast } from '@/hooks/useToast'
import { Stack, Flex, Spinner } from '@chakra-ui/react'
import { useRouter } from 'next/router'
import { useFolder } from '../hooks/useFolder'
import { TypebotDndProvider } from '../TypebotDndProvider'
import { FolderContent } from './FolderContent'
export const FolderPage = () => {
const router = useRouter()
const { showToast } = useToast()
const { folder } = useFolder({
folderId: router.query.id?.toString(),
onError: (error) => {
showToast({
title: "Couldn't fetch folder content",
description: error.message,
})
},
})
return (
<Stack minH="100vh">
<Seo title="My typebots" />
<DashboardHeader />
<TypebotDndProvider>
{!folder ? (
<Flex flex="1">
<Spinner mx="auto" />
</Flex>
) : (
<FolderContent folder={folder} />
)}
</TypebotDndProvider>
</Stack>
)
}

View File

@ -0,0 +1,29 @@
import {
ButtonProps,
IconButton,
Menu,
MenuButton,
MenuList,
} from '@chakra-ui/react'
import { MoreVerticalIcon } from '@/components/icons'
import { ReactNode } from 'react'
type Props = { children: ReactNode } & ButtonProps
export const MoreButton = ({ children, ...props }: Props) => {
return (
<Menu isLazy>
<MenuButton
data-testid="more-button"
as={IconButton}
icon={<MoreVerticalIcon />}
onClick={(e) => e.stopPropagation()}
colorScheme="gray"
variant="outline"
size="sm"
{...props}
/>
<MenuList>{children}</MenuList>
</Menu>
)
}

View File

@ -0,0 +1,183 @@
import React from 'react'
import {
Button,
Flex,
IconButton,
MenuItem,
Tag,
Text,
useDisclosure,
VStack,
WrapItem,
} from '@chakra-ui/react'
import { useRouter } from 'next/router'
import { ConfirmModal } from '@/components/ConfirmModal'
import { GripIcon } from '@/components/icons'
import { Typebot } from 'models'
import { useTypebotDnd } from '../TypebotDndProvider'
import { useDebounce } from 'use-debounce'
import { Plan } from 'db'
import { useWorkspace } from '@/features/workspace'
import { useToast } from '@/hooks/useToast'
import { isMobile } from '@/utils/helpers'
import {
getTypebotQuery,
deleteTypebotQuery,
importTypebotQuery,
} from '@/features/dashboard'
import { MoreButton } from './MoreButton'
import { EmojiOrImageIcon } from '@/components/EmojiOrImageIcon'
type ChatbotCardProps = {
typebot: Pick<Typebot, 'id' | 'publishedTypebotId' | 'name' | 'icon'>
isReadOnly?: boolean
onTypebotDeleted?: () => void
onMouseDown?: (e: React.MouseEvent<HTMLButtonElement>) => void
}
export const TypebotButton = ({
typebot,
onTypebotDeleted,
isReadOnly = false,
onMouseDown,
}: ChatbotCardProps) => {
const router = useRouter()
const { workspace } = useWorkspace()
const { draggedTypebot } = useTypebotDnd()
const [draggedTypebotDebounced] = useDebounce(draggedTypebot, 200)
const {
isOpen: isDeleteOpen,
onOpen: onDeleteOpen,
onClose: onDeleteClose,
} = useDisclosure()
const { showToast } = useToast()
const handleTypebotClick = () => {
if (draggedTypebotDebounced) return
router.push(
isMobile
? `/typebots/${typebot.id}/results`
: `/typebots/${typebot.id}/edit`
)
}
const handleDeleteTypebotClick = async () => {
if (isReadOnly) return
const { error } = await deleteTypebotQuery(typebot.id)
if (error)
return showToast({
title: "Couldn't delete typebot",
description: error.message,
})
if (onTypebotDeleted) onTypebotDeleted()
}
const handleDuplicateClick = async (e: React.MouseEvent) => {
e.stopPropagation()
const { data } = await getTypebotQuery(typebot.id)
const typebotToDuplicate = data?.typebot
if (!typebotToDuplicate) return { error: new Error('Typebot not found') }
const { data: createdTypebot, error } = await importTypebotQuery(
data.typebot,
workspace?.plan ?? Plan.FREE
)
if (error)
return showToast({
title: "Couldn't duplicate typebot",
description: error.message,
})
if (createdTypebot) router.push(`/typebots/${createdTypebot?.id}/edit`)
}
const handleDeleteClick = (e: React.MouseEvent) => {
e.stopPropagation()
onDeleteOpen()
}
return (
<Button
as={WrapItem}
onClick={handleTypebotClick}
display="flex"
flexDir="column"
variant="outline"
color="gray.800"
w="225px"
h="270px"
mr={{ sm: 6 }}
mb={6}
rounded="lg"
whiteSpace="normal"
opacity={draggedTypebot?.id === typebot.id ? 0.2 : 1}
onMouseDown={onMouseDown}
cursor="pointer"
>
{typebot.publishedTypebotId && (
<Tag
colorScheme="blue"
variant="solid"
rounded="full"
pos="absolute"
top="27px"
size="sm"
>
Live
</Tag>
)}
{!isReadOnly && (
<>
<IconButton
icon={<GripIcon />}
pos="absolute"
top="20px"
left="20px"
aria-label="Drag"
cursor="grab"
variant="ghost"
colorScheme="blue"
size="sm"
/>
<MoreButton
pos="absolute"
top="20px"
right="20px"
aria-label={`Show ${typebot.name} menu`}
>
<MenuItem onClick={handleDuplicateClick}>Duplicate</MenuItem>
<MenuItem color="red" onClick={handleDeleteClick}>
Delete
</MenuItem>
</MoreButton>
</>
)}
<VStack spacing="4">
<Flex
rounded="full"
justifyContent="center"
alignItems="center"
fontSize={'4xl'}
>
{<EmojiOrImageIcon icon={typebot.icon} boxSize={'35px'} />}
</Flex>
<Text textAlign="center">{typebot.name}</Text>
</VStack>
{!isReadOnly && (
<ConfirmModal
message={
<Text>
Are you sure you want to delete your Typebot &quot;{typebot.name}
&quot;.
<br />
All associated data will be lost.
</Text>
}
confirmButtonLabel="Delete"
onConfirm={handleDeleteTypebotClick}
isOpen={isDeleteOpen}
onClose={onDeleteClose}
/>
)}
</Button>
)
}

View File

@ -0,0 +1,47 @@
import { Box, BoxProps, Flex, Text, VStack } from '@chakra-ui/react'
import { GlobeIcon, ToolIcon } from '@/components/icons'
import { TypebotInDashboard } from '@/features/dashboard'
type Props = {
typebot: TypebotInDashboard
} & BoxProps
export const TypebotCardOverlay = ({ typebot, ...props }: Props) => {
return (
<Box
display="flex"
flexDir="column"
variant="outline"
justifyContent="center"
w="225px"
h="270px"
whiteSpace="normal"
transition="none"
pointerEvents="none"
borderWidth={1}
rounded="md"
bgColor="white"
shadow="lg"
opacity={0.7}
{...props}
>
<VStack spacing={4}>
<Flex
boxSize="45px"
rounded="full"
justifyContent="center"
alignItems="center"
bgColor={typebot.publishedTypebotId ? 'blue.500' : 'gray.400'}
color="white"
>
{typebot.publishedTypebotId ? (
<GlobeIcon fill="white" fontSize="20px" />
) : (
<ToolIcon fill="white" fontSize="20px" />
)}
</Flex>
<Text>{typebot.name}</Text>
</VStack>
</Box>
)
}

View File

@ -0,0 +1,22 @@
import { fetcher } from '@/utils/helpers'
import { DashboardFolder } from 'db'
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,
}
}

View File

@ -0,0 +1,30 @@
import { fetcher } from '@/utils/helpers'
import { DashboardFolder } from 'db'
import { stringify } from 'qs'
import useSWR from 'swr'
import { env } from 'utils'
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('E2E_TEST') === 'true' ? 0 : undefined,
}
)
if (error) onError(error)
return {
folders: data?.folders,
isLoading: !error && !data,
mutate,
}
}

View File

@ -0,0 +1,3 @@
export { FolderContent } from './components/FolderContent'
export { TypebotDndProvider } from './TypebotDndProvider'
export { FolderPage } from './components/FolderPage'

View File

@ -0,0 +1,12 @@
import { DashboardFolder } from 'db'
import { sendRequest } from 'utils'
export const createFolderQuery = async (
workspaceId: string,
folder: Pick<DashboardFolder, 'parentFolderId'>
) =>
sendRequest<DashboardFolder>({
url: `/api/folders`,
method: 'POST',
body: { ...folder, workspaceId },
})

View File

@ -0,0 +1,7 @@
import { sendRequest } from 'utils'
export const deleteFolderQuery = async (id: string) =>
sendRequest({
url: `/api/folders/${id}`,
method: 'DELETE',
})

View File

@ -0,0 +1,12 @@
import { Typebot } from 'db'
import { sendRequest } from 'utils'
export const patchTypebotQuery = async (
id: string,
typebot: Partial<Typebot>
) =>
sendRequest({
url: `/api/typebots/${id}`,
method: 'PATCH',
body: typebot,
})

View File

@ -0,0 +1,12 @@
import { DashboardFolder } from 'db'
import { sendRequest } from 'utils'
export const updateFolderQuery = async (
id: string,
folder: Partial<DashboardFolder>
) =>
sendRequest({
url: `/api/folders/${id}`,
method: 'PATCH',
body: folder,
})