♻️ (builder) Change to features-centric folder structure
This commit is contained in:
committed by
Baptiste Arnaud
parent
3686465a85
commit
643571fe7d
@ -26,5 +26,18 @@ module.exports = {
|
||||
rules: {
|
||||
'react/no-unescaped-entities': [0],
|
||||
'react/display-name': [0],
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: [
|
||||
'*/src/*',
|
||||
'src/*',
|
||||
'*/src',
|
||||
'@/features/*/*',
|
||||
'!@/features/blocks/*',
|
||||
'!@/features/*/api',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
export * from './GiphyLogo'
|
||||
export * from './GitlabLogo'
|
||||
export * from './GoogleAnalyticsLogo'
|
||||
export * from './GoogleSheetsLogo'
|
||||
export * from './GtmLogo'
|
||||
export * from './IframeLogo'
|
||||
export * from './JavascriptLogo'
|
||||
export * from './NotionLogo'
|
||||
export * from './OtherLogo'
|
||||
export * from './ReactLogo'
|
||||
export * from './ShopifyLogo'
|
||||
export * from './TypebotLogo'
|
||||
export * from './WebflowLogo'
|
||||
export * from './WordpressLogo'
|
||||
export * from './WixLogo'
|
||||
export * from './GoogleLogo'
|
||||
export * from './FacebookLogo'
|
||||
export * from './ZapierLogo'
|
||||
export * from './MakeComLogo'
|
||||
export * from './PabblyConnectLogo'
|
||||
export * from './AzureAdLogo'
|
@ -1,21 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { TextLink } from 'components/shared/TextLink'
|
||||
|
||||
type Props = {
|
||||
type: 'register' | 'signin'
|
||||
}
|
||||
export const AuthSwitcher = ({ type }: Props) => (
|
||||
<>
|
||||
{type === 'signin' ? (
|
||||
<Text>
|
||||
Don't have an account?{' '}
|
||||
<TextLink href="/register">Sign up for free</TextLink>
|
||||
</Text>
|
||||
) : (
|
||||
<Text>
|
||||
Already have an account? <TextLink href="/signin">Sign in</TextLink>
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)
|
@ -1,104 +0,0 @@
|
||||
import { IconProps } from '@chakra-ui/react'
|
||||
import {
|
||||
BoxIcon,
|
||||
CalendarIcon,
|
||||
ChatIcon,
|
||||
CheckSquareIcon,
|
||||
CodeIcon,
|
||||
CreditCardIcon,
|
||||
EditIcon,
|
||||
EmailIcon,
|
||||
ExternalLinkIcon,
|
||||
FilmIcon,
|
||||
FilterIcon,
|
||||
FlagIcon,
|
||||
GlobeIcon,
|
||||
ImageIcon,
|
||||
LayoutIcon,
|
||||
NumberIcon,
|
||||
PhoneIcon,
|
||||
SendEmailIcon,
|
||||
StarIcon,
|
||||
TextIcon,
|
||||
UploadIcon,
|
||||
WebhookIcon,
|
||||
} from 'assets/icons'
|
||||
import {
|
||||
GoogleAnalyticsLogo,
|
||||
GoogleSheetsLogo,
|
||||
MakeComLogo,
|
||||
PabblyConnectLogo,
|
||||
ZapierLogo,
|
||||
} from 'assets/logos'
|
||||
import { ChatwootLogo } from 'features/chatwoot'
|
||||
import {
|
||||
BubbleBlockType,
|
||||
InputBlockType,
|
||||
IntegrationBlockType,
|
||||
LogicBlockType,
|
||||
BlockType,
|
||||
} from 'models'
|
||||
import React from 'react'
|
||||
|
||||
type BlockIconProps = { type: BlockType } & IconProps
|
||||
|
||||
export const BlockIcon = ({ type, ...props }: BlockIconProps) => {
|
||||
switch (type) {
|
||||
case BubbleBlockType.TEXT:
|
||||
return <ChatIcon color="blue.500" {...props} />
|
||||
case BubbleBlockType.IMAGE:
|
||||
return <ImageIcon color="blue.500" {...props} />
|
||||
case BubbleBlockType.VIDEO:
|
||||
return <FilmIcon color="blue.500" {...props} />
|
||||
case BubbleBlockType.EMBED:
|
||||
return <LayoutIcon color="blue.500" {...props} />
|
||||
case InputBlockType.TEXT:
|
||||
return <TextIcon color="orange.500" {...props} />
|
||||
case InputBlockType.NUMBER:
|
||||
return <NumberIcon color="orange.500" {...props} />
|
||||
case InputBlockType.EMAIL:
|
||||
return <EmailIcon color="orange.500" {...props} />
|
||||
case InputBlockType.URL:
|
||||
return <GlobeIcon color="orange.500" {...props} />
|
||||
case InputBlockType.DATE:
|
||||
return <CalendarIcon color="orange.500" {...props} />
|
||||
case InputBlockType.PHONE:
|
||||
return <PhoneIcon color="orange.500" {...props} />
|
||||
case InputBlockType.CHOICE:
|
||||
return <CheckSquareIcon color="orange.500" {...props} />
|
||||
case InputBlockType.PAYMENT:
|
||||
return <CreditCardIcon color="orange.500" {...props} />
|
||||
case InputBlockType.RATING:
|
||||
return <StarIcon color="orange.500" {...props} />
|
||||
case InputBlockType.FILE:
|
||||
return <UploadIcon color="orange.500" {...props} />
|
||||
case LogicBlockType.SET_VARIABLE:
|
||||
return <EditIcon color="purple.500" {...props} />
|
||||
case LogicBlockType.CONDITION:
|
||||
return <FilterIcon color="purple.500" {...props} />
|
||||
case LogicBlockType.REDIRECT:
|
||||
return <ExternalLinkIcon color="purple.500" {...props} />
|
||||
case LogicBlockType.CODE:
|
||||
return <CodeIcon color="purple.500" {...props} />
|
||||
case LogicBlockType.TYPEBOT_LINK:
|
||||
return <BoxIcon color="purple.500" {...props} />
|
||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||
return <GoogleSheetsLogo {...props} />
|
||||
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
||||
return <GoogleAnalyticsLogo {...props} />
|
||||
case IntegrationBlockType.WEBHOOK:
|
||||
return <WebhookIcon {...props} />
|
||||
case IntegrationBlockType.ZAPIER:
|
||||
return <ZapierLogo {...props} />
|
||||
case IntegrationBlockType.MAKE_COM:
|
||||
return <MakeComLogo {...props} />
|
||||
case IntegrationBlockType.PABBLY_CONNECT:
|
||||
return <PabblyConnectLogo {...props} />
|
||||
case IntegrationBlockType.EMAIL:
|
||||
return <SendEmailIcon {...props} />
|
||||
case IntegrationBlockType.CHATWOOT:
|
||||
return <ChatwootLogo {...props} />
|
||||
case 'start':
|
||||
return <FlagIcon {...props} />
|
||||
}
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
import {
|
||||
Flex,
|
||||
Heading,
|
||||
HStack,
|
||||
IconButton,
|
||||
Stack,
|
||||
Wrap,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { TrashIcon } from 'assets/icons'
|
||||
import { UpgradeButton } from 'components/shared/buttons/UpgradeButton'
|
||||
import { useToast } from 'components/shared/hooks/useToast'
|
||||
import { LockTag } from 'components/shared/LockTag'
|
||||
import { LimitReached } from 'components/shared/modals/ChangePlanModal'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { useWorkspace } from 'contexts/WorkspaceContext'
|
||||
import { Plan } from 'db'
|
||||
import React from 'react'
|
||||
import { parseDefaultPublicId } from 'services/typebots'
|
||||
import { isWorkspaceProPlan } from 'services/workspace'
|
||||
import { getViewerUrl, isDefined, isNotDefined } from 'utils'
|
||||
import { CustomDomainsDropdown } from './customDomain/CustomDomainsDropdown'
|
||||
import { EditableUrl } from './EditableUrl'
|
||||
import { integrationsList } from './integrations/EmbedButton'
|
||||
import { isPublicDomainAvailableQuery } from './queries/isPublicDomainAvailableQuery'
|
||||
|
||||
export const ShareContent = () => {
|
||||
const { workspace } = useWorkspace()
|
||||
const { typebot, updateTypebot } = useTypebot()
|
||||
const { showToast } = useToast()
|
||||
|
||||
const handlePublicIdChange = async (publicId: string) => {
|
||||
if (publicId === typebot?.publicId) return
|
||||
if (publicId.length < 4)
|
||||
return showToast({ description: 'ID must be longer than 4 characters' })
|
||||
|
||||
const { data } = await isPublicDomainAvailableQuery(publicId)
|
||||
if (!data?.isAvailable)
|
||||
return showToast({ description: 'ID is already taken' })
|
||||
|
||||
updateTypebot({ publicId })
|
||||
}
|
||||
|
||||
const publicId = typebot
|
||||
? typebot?.publicId ?? parseDefaultPublicId(typebot.name, typebot.id)
|
||||
: ''
|
||||
const isPublished = isDefined(typebot?.publishedTypebotId)
|
||||
|
||||
const handlePathnameChange = (pathname: string) => {
|
||||
if (!typebot?.customDomain) return
|
||||
const existingHost = typebot.customDomain?.split('/')[0]
|
||||
const newDomain =
|
||||
pathname === '' ? existingHost : existingHost + '/' + pathname
|
||||
handleCustomDomainChange(newDomain)
|
||||
}
|
||||
|
||||
const handleCustomDomainChange = (customDomain: string | undefined) =>
|
||||
updateTypebot({ customDomain })
|
||||
|
||||
return (
|
||||
<Flex h="full" w="full" justifyContent="center" align="flex-start">
|
||||
<Stack maxW="1000px" w="full" pt="10" spacing={10}>
|
||||
<Stack spacing={4} align="flex-start">
|
||||
<Heading fontSize="2xl" as="h1">
|
||||
Your typebot link
|
||||
</Heading>
|
||||
{typebot && (
|
||||
<EditableUrl
|
||||
hostname={
|
||||
getViewerUrl({ isBuilder: true }) ?? 'https://typebot.io'
|
||||
}
|
||||
pathname={publicId}
|
||||
onPathnameChange={handlePublicIdChange}
|
||||
/>
|
||||
)}
|
||||
{typebot?.customDomain && (
|
||||
<HStack>
|
||||
<EditableUrl
|
||||
hostname={'https://' + typebot.customDomain.split('/')[0]}
|
||||
pathname={typebot.customDomain.split('/')[1]}
|
||||
onPathnameChange={handlePathnameChange}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
aria-label="Remove custom domain"
|
||||
size="xs"
|
||||
onClick={() => handleCustomDomainChange(undefined)}
|
||||
/>
|
||||
</HStack>
|
||||
)}
|
||||
{isNotDefined(typebot?.customDomain) ? (
|
||||
<>
|
||||
{isWorkspaceProPlan(workspace) ? (
|
||||
<CustomDomainsDropdown
|
||||
onCustomDomainSelect={handleCustomDomainChange}
|
||||
/>
|
||||
) : (
|
||||
<UpgradeButton
|
||||
colorScheme="gray"
|
||||
limitReachedType={LimitReached.CUSTOM_DOMAIN}
|
||||
>
|
||||
<Text mr="2">Add my domain</Text> <LockTag plan={Plan.PRO} />
|
||||
</UpgradeButton>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</Stack>
|
||||
|
||||
<Stack spacing={4}>
|
||||
<Heading fontSize="2xl" as="h1">
|
||||
Embed your typebot
|
||||
</Heading>
|
||||
<Wrap spacing={7}>
|
||||
{integrationsList.map((IntegrationButton, idx) => (
|
||||
<IntegrationButton
|
||||
key={idx}
|
||||
publicId={publicId}
|
||||
isPublished={isPublished}
|
||||
/>
|
||||
))}
|
||||
</Wrap>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Flex>
|
||||
)
|
||||
}
|
@ -1,164 +0,0 @@
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { ChatwootBlockNodeLabel } from 'features/chatwoot'
|
||||
import {
|
||||
Block,
|
||||
StartBlock,
|
||||
BubbleBlockType,
|
||||
InputBlockType,
|
||||
LogicBlockType,
|
||||
IntegrationBlockType,
|
||||
BlockIndices,
|
||||
} from 'models'
|
||||
import { isChoiceInput, isInputBlock } from 'utils'
|
||||
import { ItemNodesList } from '../../ItemNode'
|
||||
import {
|
||||
EmbedBubbleContent,
|
||||
SetVariableContent,
|
||||
TextBubbleContent,
|
||||
VideoBubbleContent,
|
||||
WebhookContent,
|
||||
WithVariableContent,
|
||||
} from './contents'
|
||||
import { ConfigureContent } from './contents/ConfigureContent'
|
||||
import { FileInputContent } from './contents/FileInputContent'
|
||||
import { ImageBubbleContent } from './contents/ImageBubbleContent'
|
||||
import { PaymentInputContent } from './contents/PaymentInputContent'
|
||||
import { PlaceholderContent } from './contents/PlaceholderContent'
|
||||
import { RatingInputContent } from './contents/RatingInputContent'
|
||||
import { SendEmailContent } from './contents/SendEmailContent'
|
||||
import { TypebotLinkContent } from './contents/TypebotLinkContent'
|
||||
import { ProviderWebhookContent } from './contents/ZapierContent'
|
||||
|
||||
type Props = {
|
||||
block: Block | StartBlock
|
||||
indices: BlockIndices
|
||||
}
|
||||
export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
|
||||
if (
|
||||
isInputBlock(block) &&
|
||||
!isChoiceInput(block) &&
|
||||
block.options.variableId
|
||||
) {
|
||||
return <WithVariableContent block={block} />
|
||||
}
|
||||
switch (block.type) {
|
||||
case BubbleBlockType.TEXT: {
|
||||
return <TextBubbleContent block={block} />
|
||||
}
|
||||
case BubbleBlockType.IMAGE: {
|
||||
return <ImageBubbleContent block={block} />
|
||||
}
|
||||
case BubbleBlockType.VIDEO: {
|
||||
return <VideoBubbleContent block={block} />
|
||||
}
|
||||
case BubbleBlockType.EMBED: {
|
||||
return <EmbedBubbleContent block={block} />
|
||||
}
|
||||
case InputBlockType.TEXT: {
|
||||
return (
|
||||
<PlaceholderContent
|
||||
placeholder={block.options.labels.placeholder}
|
||||
isLong={block.options.isLong}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case InputBlockType.NUMBER:
|
||||
case InputBlockType.EMAIL:
|
||||
case InputBlockType.URL:
|
||||
case InputBlockType.PHONE: {
|
||||
return (
|
||||
<PlaceholderContent placeholder={block.options.labels.placeholder} />
|
||||
)
|
||||
}
|
||||
case InputBlockType.DATE: {
|
||||
return <Text color={'gray.500'}>Pick a date...</Text>
|
||||
}
|
||||
case InputBlockType.CHOICE: {
|
||||
return <ItemNodesList block={block} indices={indices} />
|
||||
}
|
||||
case InputBlockType.PAYMENT: {
|
||||
return <PaymentInputContent block={block} />
|
||||
}
|
||||
case InputBlockType.RATING: {
|
||||
return <RatingInputContent block={block} />
|
||||
}
|
||||
case InputBlockType.FILE: {
|
||||
return <FileInputContent options={block.options} />
|
||||
}
|
||||
case LogicBlockType.SET_VARIABLE: {
|
||||
return <SetVariableContent block={block} />
|
||||
}
|
||||
case LogicBlockType.CONDITION: {
|
||||
return <ItemNodesList block={block} indices={indices} isReadOnly />
|
||||
}
|
||||
case LogicBlockType.REDIRECT: {
|
||||
return (
|
||||
<ConfigureContent
|
||||
label={
|
||||
block.options?.url ? `Redirect to ${block.options?.url}` : undefined
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case LogicBlockType.CODE: {
|
||||
return (
|
||||
<ConfigureContent
|
||||
label={
|
||||
block.options?.content ? `Run ${block.options?.name}` : undefined
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case LogicBlockType.TYPEBOT_LINK:
|
||||
return <TypebotLinkContent block={block} />
|
||||
|
||||
case IntegrationBlockType.GOOGLE_SHEETS: {
|
||||
return (
|
||||
<ConfigureContent
|
||||
label={
|
||||
block.options && 'action' in block.options
|
||||
? block.options.action
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case IntegrationBlockType.GOOGLE_ANALYTICS: {
|
||||
return (
|
||||
<ConfigureContent
|
||||
label={
|
||||
block.options?.action
|
||||
? `Track "${block.options?.action}" `
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case IntegrationBlockType.WEBHOOK: {
|
||||
return <WebhookContent block={block} />
|
||||
}
|
||||
case IntegrationBlockType.ZAPIER: {
|
||||
return (
|
||||
<ProviderWebhookContent block={block} configuredLabel="Trigger zap" />
|
||||
)
|
||||
}
|
||||
case IntegrationBlockType.PABBLY_CONNECT:
|
||||
case IntegrationBlockType.MAKE_COM: {
|
||||
return (
|
||||
<ProviderWebhookContent
|
||||
block={block}
|
||||
configuredLabel="Trigger scenario"
|
||||
/>
|
||||
)
|
||||
}
|
||||
case IntegrationBlockType.EMAIL: {
|
||||
return <SendEmailContent block={block} />
|
||||
}
|
||||
case IntegrationBlockType.CHATWOOT: {
|
||||
return <ChatwootBlockNodeLabel block={block} />
|
||||
}
|
||||
case 'start': {
|
||||
return <Text>Start</Text>
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Text } from '@chakra-ui/react'
|
||||
|
||||
type Props = { label?: string }
|
||||
|
||||
export const ConfigureContent = ({ label }: Props) => (
|
||||
<Text color={label ? 'currentcolor' : 'gray.500'} noOfLines={1}>
|
||||
{label ?? 'Configure...'}
|
||||
</Text>
|
||||
)
|
@ -1,10 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Text } from '@chakra-ui/react'
|
||||
|
||||
type Props = { placeholder: string; isLong?: boolean }
|
||||
|
||||
export const PlaceholderContent = ({ placeholder, isLong }: Props) => (
|
||||
<Text color={'gray.500'} h={isLong ? '100px' : 'auto'}>
|
||||
{placeholder}
|
||||
</Text>
|
||||
)
|
@ -1,6 +0,0 @@
|
||||
export * from './SetVariableContent'
|
||||
export * from './WithVariableContent'
|
||||
export * from './VideoBubbleContent'
|
||||
export * from './WebhookContent'
|
||||
export * from './TextBubbleContent'
|
||||
export * from './EmbedBubbleContent'
|
@ -1 +0,0 @@
|
||||
export { MediaBubblePopoverContent } from './MediaBubblePopoverContent'
|
@ -1,5 +0,0 @@
|
||||
export * from './DateInputSettingsBody'
|
||||
export * from './EmailInputSettingsBody'
|
||||
export * from './NumberInputSettingsBody'
|
||||
export * from './TextInputSettingsBody'
|
||||
export * from './UrlInputSettingsBody'
|
@ -1,71 +0,0 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
KBarPortal,
|
||||
KBarPositioner,
|
||||
KBarAnimator,
|
||||
KBarSearch,
|
||||
KBarResults,
|
||||
useMatches,
|
||||
} from 'kbar'
|
||||
import { chakra, Flex } from '@chakra-ui/react'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
type KBarProps = {}
|
||||
|
||||
const KBarSearchChakra = chakra(KBarSearch)
|
||||
const KBarAnimatorChakra = chakra(KBarAnimator)
|
||||
const KBarResultsChakra = chakra(KBarResults)
|
||||
|
||||
export const KBar = ({}: KBarProps) => {
|
||||
return (
|
||||
<KBarPortal>
|
||||
<KBarPositioner>
|
||||
<KBarAnimatorChakra shadow="2xl" rounded="md">
|
||||
<KBarSearchChakra
|
||||
p={4}
|
||||
w="500px"
|
||||
roundedTop="md"
|
||||
_focus={{ outline: 'none' }}
|
||||
/>
|
||||
<RenderResults />
|
||||
</KBarAnimatorChakra>
|
||||
</KBarPositioner>
|
||||
</KBarPortal>
|
||||
)
|
||||
}
|
||||
|
||||
const RenderResults = () => {
|
||||
const { results } = useMatches()
|
||||
|
||||
return (
|
||||
<KBarResultsChakra
|
||||
items={results}
|
||||
onRender={({ item, active }) =>
|
||||
typeof item === 'string' ? (
|
||||
<Flex height="50px">{item}</Flex>
|
||||
) : (
|
||||
<Flex
|
||||
height="50px"
|
||||
roundedBottom="md"
|
||||
align="center"
|
||||
px="4"
|
||||
bgColor={active ? 'blue.50' : 'white'}
|
||||
_hover={{ bgColor: 'blue.50' }}
|
||||
>
|
||||
{active && (
|
||||
<Flex
|
||||
pos="absolute"
|
||||
left="0"
|
||||
h="full"
|
||||
w="3px"
|
||||
roundedRight="md"
|
||||
bgColor={'blue.500'}
|
||||
/>
|
||||
)}
|
||||
{item.name}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { Heading, Text, VStack } from '@chakra-ui/react'
|
||||
import { TypebotLogo } from 'assets/logos'
|
||||
import React from 'react'
|
||||
|
||||
export const MaintenancePage = () => (
|
||||
<VStack h="100vh" justify="center">
|
||||
<TypebotLogo />
|
||||
<Heading>
|
||||
The tool is under maintenance for an exciting new feature! 🤩
|
||||
</Heading>
|
||||
<Text>Please come back again in 10 minutes.</Text>
|
||||
</VStack>
|
||||
)
|
@ -1 +0,0 @@
|
||||
export { ChangePlanModal } from './ChangePlanModal'
|
@ -1 +0,0 @@
|
||||
export { TypebotContext, useTypebot } from './TypebotContext'
|
@ -1,3 +0,0 @@
|
||||
export { ChatwootLogo } from './ChatwootLogo'
|
||||
export { ChatwootBlockNodeLabel } from './ChatwootBlockNodeLabel'
|
||||
export { ChatwootSettingsForm } from './ChatwootSettingsForm'
|
@ -1 +0,0 @@
|
||||
export * from './components'
|
@ -1,11 +0,0 @@
|
||||
import { TypebotLogo } from 'assets/logos'
|
||||
import { Spinner, VStack } from '@chakra-ui/react'
|
||||
|
||||
export const LoadingPage = () => (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<VStack spacing={6}>
|
||||
<TypebotLogo boxSize="80px" />
|
||||
<Spinner />
|
||||
</VStack>
|
||||
</div>
|
||||
)
|
@ -1,30 +0,0 @@
|
||||
import { Flex } from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import React, { useMemo } from 'react'
|
||||
import { TypebotViewer } from 'bot-engine'
|
||||
import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
|
||||
import { SettingsSideMenu } from 'components/settings/SettingsSideMenu'
|
||||
import { getViewerUrl } from 'utils'
|
||||
|
||||
export const SettingsContent = () => {
|
||||
const { typebot } = useTypebot()
|
||||
const publicTypebot = useMemo(
|
||||
() => (typebot ? parseTypebotToPublicTypebot(typebot) : undefined),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[typebot?.settings]
|
||||
)
|
||||
|
||||
return (
|
||||
<Flex h="full" w="full">
|
||||
<SettingsSideMenu />
|
||||
<Flex flex="1">
|
||||
{publicTypebot && (
|
||||
<TypebotViewer
|
||||
apiHost={getViewerUrl({ isBuilder: true })}
|
||||
typebot={publicTypebot}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import { Flex } from '@chakra-ui/react'
|
||||
import { TypebotViewer } from 'bot-engine'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import React from 'react'
|
||||
import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
|
||||
import { getViewerUrl } from 'utils'
|
||||
import { ThemeSideMenu } from '../../components/theme/ThemeSideMenu'
|
||||
|
||||
export const ThemeContent = () => {
|
||||
const { typebot } = useTypebot()
|
||||
const publicTypebot = typebot && parseTypebotToPublicTypebot(typebot)
|
||||
return (
|
||||
<Flex h="full" w="full">
|
||||
<ThemeSideMenu />
|
||||
<Flex flex="1">
|
||||
{publicTypebot && (
|
||||
<TypebotViewer
|
||||
apiHost={getViewerUrl({ isBuilder: true })}
|
||||
typebot={publicTypebot}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
@ -1 +0,0 @@
|
||||
export const actions = []
|
@ -56,7 +56,6 @@
|
||||
"immer": "9.0.16",
|
||||
"js-video-url-parser": "0.5.1",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"kbar": "0.1.0-beta.36",
|
||||
"micro": "9.4.1",
|
||||
"micro-cors": "0.1.1",
|
||||
"minio": "7.0.32",
|
||||
|
@ -1,21 +0,0 @@
|
||||
import { AuthSwitcher } from 'components/auth/AuthSwitcher'
|
||||
import { SignInForm } from 'components/auth/SignInForm'
|
||||
import { Heading, VStack } from '@chakra-ui/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { Seo } from 'components/Seo'
|
||||
|
||||
const RegisterPage = () => {
|
||||
const { query } = useRouter()
|
||||
|
||||
return (
|
||||
<VStack spacing={4} h="100vh" justifyContent="center">
|
||||
<Seo title="Register" />
|
||||
<Heading>Create an account</Heading>
|
||||
<AuthSwitcher type="register" />
|
||||
<SignInForm defaultEmail={query.g?.toString()} />
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
export default RegisterPage
|
@ -1,24 +0,0 @@
|
||||
import { AuthSwitcher } from 'components/auth/AuthSwitcher'
|
||||
import { SignInForm } from 'components/auth/SignInForm'
|
||||
import { Heading, VStack } from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import { Seo } from 'components/Seo'
|
||||
|
||||
const SignInPage = () => {
|
||||
return (
|
||||
<VStack spacing={4} h="100vh" justifyContent="center">
|
||||
<Seo title="Sign in" />
|
||||
<Heading
|
||||
onClick={() => {
|
||||
throw new Error('Sentry is working')
|
||||
}}
|
||||
>
|
||||
Sign in
|
||||
</Heading>
|
||||
<AuthSwitcher type="signin" />
|
||||
<SignInForm />
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
export default SignInPage
|
@ -1,15 +0,0 @@
|
||||
import { Flex } from '@chakra-ui/react'
|
||||
import { Seo } from 'components/Seo'
|
||||
import { SettingsContent } from 'layouts/settings/SettingsContent'
|
||||
import { TypebotHeader } from 'components/shared/TypebotHeader'
|
||||
import React from 'react'
|
||||
|
||||
const SettingsPage = () => (
|
||||
<Flex overflow="hidden" h="100vh" flexDir="column">
|
||||
<Seo title="Settings" />
|
||||
<TypebotHeader />
|
||||
<SettingsContent />
|
||||
</Flex>
|
||||
)
|
||||
|
||||
export default SettingsPage
|
@ -1,15 +0,0 @@
|
||||
import { Flex } from '@chakra-ui/react'
|
||||
import { Seo } from 'components/Seo'
|
||||
import { ShareContent } from 'components/share/ShareContent'
|
||||
import { TypebotHeader } from 'components/shared/TypebotHeader'
|
||||
import React from 'react'
|
||||
|
||||
const SharePage = () => (
|
||||
<Flex flexDir="column" pb="40">
|
||||
<Seo title="Share" />
|
||||
<TypebotHeader />
|
||||
<ShareContent />
|
||||
</Flex>
|
||||
)
|
||||
|
||||
export default SharePage
|
@ -1,15 +0,0 @@
|
||||
import { Flex } from '@chakra-ui/react'
|
||||
import { Seo } from 'components/Seo'
|
||||
import { TypebotHeader } from 'components/shared/TypebotHeader'
|
||||
import { ThemeContent } from 'layouts/theme/ThemeContent'
|
||||
import React from 'react'
|
||||
|
||||
const ThemePage = () => (
|
||||
<Flex overflow="hidden" h="100vh" flexDir="column">
|
||||
<Seo title="Theme" />
|
||||
<TypebotHeader />
|
||||
<ThemeContent />
|
||||
</Flex>
|
||||
)
|
||||
|
||||
export default ThemePage
|
@ -1,15 +0,0 @@
|
||||
import React from 'react'
|
||||
import { VStack } from '@chakra-ui/react'
|
||||
import { Seo } from 'components/Seo'
|
||||
import { DashboardHeader } from 'components/dashboard/DashboardHeader'
|
||||
import { CreateNewTypebotButtons } from 'components/templates/CreateNewTypebotButtons'
|
||||
|
||||
const TemplatesPage = () => (
|
||||
<VStack>
|
||||
<Seo title="Templates" />
|
||||
<DashboardHeader />
|
||||
<CreateNewTypebotButtons />
|
||||
</VStack>
|
||||
)
|
||||
|
||||
export default TemplatesPage
|
@ -1,44 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Flex, Stack } from '@chakra-ui/react'
|
||||
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 } from '@chakra-ui/react'
|
||||
import { TypebotDndContext } from 'contexts/TypebotDndContext'
|
||||
import { useToast } from 'components/shared/hooks/useToast'
|
||||
|
||||
const FolderPage = () => {
|
||||
const router = useRouter()
|
||||
|
||||
const { showToast } = useToast()
|
||||
|
||||
const { folder } = useFolderContent({
|
||||
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 />
|
||||
<TypebotDndContext>
|
||||
{!folder ? (
|
||||
<Flex flex="1">
|
||||
<Spinner mx="auto" />
|
||||
</Flex>
|
||||
) : (
|
||||
<FolderContent folder={folder} />
|
||||
)}
|
||||
</TypebotDndContext>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export default FolderPage
|
@ -13,9 +13,9 @@ const config: PlaywrightTestConfig = {
|
||||
use: {
|
||||
...playwrightBaseConfig.use,
|
||||
baseURL: process.env.NEXTAUTH_URL,
|
||||
storageState: path.join(__dirname, 'playwright/firstUser.json'),
|
||||
storageState: path.join(__dirname, 'src/test/storageState.json'),
|
||||
},
|
||||
outputDir: path.join(__dirname, 'playwright/test-results/'),
|
||||
outputDir: path.join(__dirname, 'src/test/results/'),
|
||||
}
|
||||
|
||||
export default config
|
||||
|
@ -1,14 +0,0 @@
|
||||
PLAYWRIGHT_BUILDER_TEST_BASE_URL=http://localhost:3000
|
||||
DATABASE_URL=postgresql://postgres:typebot@localhost:5432/typebot
|
||||
ENCRYPTION_SECRET=SgVkYp2s5v8y/B?E(H+MbQeThWmZq4t6 #256-bits secret (can be generated here: https://www.allkeysgenerator.com/Random/Security-Encryption-Key-Generator.aspx)
|
||||
|
||||
# SMTP Credentials (Generated on https://ethereal.email/)
|
||||
SMTP_HOST=smtp.ethereal.email
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=true
|
||||
SMTP_USERNAME=tobin.tillman65@ethereal.email
|
||||
SMTP_PASSWORD=Ty9BcwCBrK6w8AG2hx
|
||||
|
||||
STRIPE_TEST_PUBLIC_KEY=
|
||||
STRIPE_TEST_SECRET_KEY=
|
||||
STRIPE_STARTER_PRICE_ID=
|
@ -1,15 +0,0 @@
|
||||
{
|
||||
"cookies": [],
|
||||
"origins": [
|
||||
{
|
||||
"origin": "http://localhost:3000",
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "typebot-20-modal",
|
||||
"value": "hide"
|
||||
},
|
||||
{ "name": "workspaceId", "value": "freeWorkspace" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
import { Stats } from 'models'
|
||||
import useSWR from 'swr'
|
||||
import { fetcher } from './utils'
|
||||
|
||||
export const useStats = ({
|
||||
typebotId,
|
||||
onError,
|
||||
}: {
|
||||
typebotId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<{ stats: Stats }, Error>(
|
||||
typebotId ? `/api/typebots/${typebotId}/results/stats` : null,
|
||||
fetcher
|
||||
)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
stats: data?.stats,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
export type AnswersCount = { groupId: string; totalAnswers: number }
|
||||
export const useAnswersCount = ({
|
||||
typebotId,
|
||||
onError,
|
||||
}: {
|
||||
typebotId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<
|
||||
{ answersCounts: AnswersCount[] },
|
||||
Error
|
||||
>(
|
||||
typebotId ? `/api/typebots/${typebotId}/results/answers/count` : null,
|
||||
fetcher
|
||||
)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
answersCounts: data?.answersCounts,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const redeemCoupon = async (code: string) =>
|
||||
sendRequest<{ message: string }>({
|
||||
method: 'POST',
|
||||
url: '/api/coupons/redeem',
|
||||
body: { code },
|
||||
})
|
@ -1,48 +0,0 @@
|
||||
import { Credentials } from 'models'
|
||||
import { stringify } from 'qs'
|
||||
import useSWR from 'swr'
|
||||
import { sendRequest } from 'utils'
|
||||
import { fetcher } from './utils'
|
||||
|
||||
export const useCredentials = ({
|
||||
workspaceId,
|
||||
onError,
|
||||
}: {
|
||||
workspaceId?: string
|
||||
onError?: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<{ credentials: Credentials[] }, Error>(
|
||||
workspaceId ? `/api/credentials?${stringify({ workspaceId })}` : null,
|
||||
fetcher
|
||||
)
|
||||
if (error && onError) onError(error)
|
||||
return {
|
||||
credentials: data?.credentials ?? [],
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
export const createCredentials = async (
|
||||
credentials: Omit<Credentials, 'id' | 'iv' | 'createdAt'>
|
||||
) =>
|
||||
sendRequest<{
|
||||
credentials: Credentials
|
||||
}>({
|
||||
url: `/api/credentials?${stringify({
|
||||
workspaceId: credentials.workspaceId,
|
||||
})}`,
|
||||
method: 'POST',
|
||||
body: credentials,
|
||||
})
|
||||
|
||||
export const deleteCredentials = async (
|
||||
workspaceId: string,
|
||||
credentialsId: string
|
||||
) =>
|
||||
sendRequest<{
|
||||
credentials: Credentials
|
||||
}>({
|
||||
url: `/api/credentials/${credentialsId}?${stringify({ workspaceId })}`,
|
||||
method: 'DELETE',
|
||||
})
|
@ -1,51 +0,0 @@
|
||||
import { CustomDomain } from 'db'
|
||||
import { Credentials } from 'models'
|
||||
import { stringify } from 'qs'
|
||||
import useSWR from 'swr'
|
||||
import { sendRequest } from 'utils'
|
||||
import { fetcher } from './utils'
|
||||
|
||||
export const useCustomDomains = ({
|
||||
workspaceId,
|
||||
onError,
|
||||
}: {
|
||||
workspaceId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<
|
||||
{ customDomains: Omit<CustomDomain, 'createdAt'>[] },
|
||||
Error
|
||||
>(
|
||||
workspaceId ? `/api/customDomains?${stringify({ workspaceId })}` : null,
|
||||
fetcher
|
||||
)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
customDomains: data?.customDomains,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
export const createCustomDomain = async (
|
||||
workspaceId: string,
|
||||
customDomain: Omit<CustomDomain, 'createdAt' | 'workspaceId'>
|
||||
) =>
|
||||
sendRequest<{
|
||||
credentials: Credentials
|
||||
}>({
|
||||
url: `/api/customDomains?${stringify({ workspaceId })}`,
|
||||
method: 'POST',
|
||||
body: customDomain,
|
||||
})
|
||||
|
||||
export const deleteCustomDomain = async (
|
||||
workspaceId: string,
|
||||
customDomain: string
|
||||
) =>
|
||||
sendRequest<{
|
||||
credentials: Credentials
|
||||
}>({
|
||||
url: `/api/customDomains/${customDomain}?${stringify({ workspaceId })}`,
|
||||
method: 'DELETE',
|
||||
})
|
@ -1,75 +0,0 @@
|
||||
import { DashboardFolder } from 'db'
|
||||
import useSWR from 'swr'
|
||||
import { fetcher } from './utils'
|
||||
import { stringify } from 'qs'
|
||||
import { env, sendRequest } 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,
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
workspaceId: string,
|
||||
folder: Pick<DashboardFolder, 'parentFolderId'>
|
||||
) =>
|
||||
sendRequest<DashboardFolder>({
|
||||
url: `/api/folders`,
|
||||
method: 'POST',
|
||||
body: { ...folder, workspaceId },
|
||||
})
|
||||
|
||||
export const deleteFolder = async (id: string) =>
|
||||
sendRequest({
|
||||
url: `/api/folders/${id}`,
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
export const updateFolder = async (
|
||||
id: string,
|
||||
folder: Partial<DashboardFolder>
|
||||
) =>
|
||||
sendRequest({
|
||||
url: `/api/folders/${id}`,
|
||||
method: 'PATCH',
|
||||
body: folder,
|
||||
})
|
@ -1,143 +0,0 @@
|
||||
import { getViewerUrl, sendRequest } from 'utils'
|
||||
import { stringify } from 'qs'
|
||||
import useSWR from 'swr'
|
||||
import { fetcher } from './utils'
|
||||
import {
|
||||
SmtpCredentialsData,
|
||||
Variable,
|
||||
VariableForTest,
|
||||
WebhookResponse,
|
||||
} from 'models'
|
||||
|
||||
export const getGoogleSheetsConsentScreenUrl = (
|
||||
redirectUrl: string,
|
||||
blockId: string,
|
||||
workspaceId?: string
|
||||
) => {
|
||||
const queryParams = stringify({ redirectUrl, blockId, workspaceId })
|
||||
return `/api/credentials/google-sheets/consent-url?${queryParams}`
|
||||
}
|
||||
|
||||
export const createSheetsAccount = async (code: string) => {
|
||||
const queryParams = stringify({ code })
|
||||
return sendRequest({
|
||||
url: `/api/credentials/google-sheets/callback?${queryParams}`,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
export type Spreadsheet = { id: string; name: string }
|
||||
export const useSpreadsheets = ({
|
||||
credentialsId,
|
||||
onError,
|
||||
}: {
|
||||
credentialsId: string
|
||||
onError?: (error: Error) => void
|
||||
}) => {
|
||||
const queryParams = stringify({ credentialsId })
|
||||
const { data, error, mutate } = useSWR<{ files: Spreadsheet[] }, Error>(
|
||||
`/api/integrations/google-sheets/spreadsheets?${queryParams}`,
|
||||
fetcher
|
||||
)
|
||||
if (error) onError && onError(error)
|
||||
return {
|
||||
spreadsheets: data?.files,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
export type Sheet = { id: string; name: string; columns: string[] }
|
||||
export const useSheets = ({
|
||||
credentialsId,
|
||||
spreadsheetId,
|
||||
onError,
|
||||
}: {
|
||||
credentialsId?: string
|
||||
spreadsheetId?: string
|
||||
onError?: (error: Error) => void
|
||||
}) => {
|
||||
const queryParams = stringify({ credentialsId })
|
||||
const { data, error, mutate } = useSWR<{ sheets: Sheet[] }, Error>(
|
||||
!credentialsId || !spreadsheetId
|
||||
? null
|
||||
: `/api/integrations/google-sheets/spreadsheets/${spreadsheetId}/sheets?${queryParams}`,
|
||||
fetcher
|
||||
)
|
||||
if (error) onError && onError(error)
|
||||
return {
|
||||
sheets: data?.sheets,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
export const executeWebhook = (
|
||||
typebotId: string,
|
||||
variables: Variable[],
|
||||
{ blockId }: { blockId: string }
|
||||
) =>
|
||||
sendRequest<WebhookResponse>({
|
||||
url: `${getViewerUrl({
|
||||
isBuilder: true,
|
||||
})}/api/typebots/${typebotId}/blocks/${blockId}/executeWebhook`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
variables,
|
||||
},
|
||||
})
|
||||
|
||||
export const convertVariableForTestToVariables = (
|
||||
variablesForTest: VariableForTest[],
|
||||
variables: Variable[]
|
||||
): Variable[] => {
|
||||
if (!variablesForTest) return []
|
||||
return [
|
||||
...variables,
|
||||
...variablesForTest
|
||||
.filter((v) => v.variableId)
|
||||
.map((variableForTest) => {
|
||||
const variable = variables.find(
|
||||
(v) => v.id === variableForTest.variableId
|
||||
) as Variable
|
||||
return { ...variable, value: variableForTest.value }
|
||||
}, {}),
|
||||
]
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const getDeepKeys = (obj: any): string[] => {
|
||||
let keys: string[] = []
|
||||
for (const key in obj) {
|
||||
if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
|
||||
const subkeys = getDeepKeys(obj[key])
|
||||
keys = keys.concat(
|
||||
subkeys.map(function (subkey) {
|
||||
return key + '.' + subkey
|
||||
})
|
||||
)
|
||||
} else if (Array.isArray(obj[key])) {
|
||||
for (let i = 0; i < obj[key].length; i++) {
|
||||
const subkeys = getDeepKeys(obj[key][i])
|
||||
keys = keys.concat(
|
||||
subkeys.map(function (subkey) {
|
||||
return key + '[' + i + ']' + '.' + subkey
|
||||
})
|
||||
)
|
||||
}
|
||||
} else {
|
||||
keys.push(key)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
export const testSmtpConfig = (smtpData: SmtpCredentialsData, to: string) =>
|
||||
sendRequest({
|
||||
method: 'POST',
|
||||
url: '/api/integrations/email/test-config',
|
||||
body: {
|
||||
...smtpData,
|
||||
to,
|
||||
},
|
||||
})
|
@ -1,61 +0,0 @@
|
||||
import cuid from 'cuid'
|
||||
import { PublicTypebot, Typebot } from 'models'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export const parseTypebotToPublicTypebot = (
|
||||
typebot: Typebot
|
||||
): PublicTypebot => ({
|
||||
id: cuid(),
|
||||
typebotId: typebot.id,
|
||||
groups: typebot.groups,
|
||||
edges: typebot.edges,
|
||||
settings: typebot.settings,
|
||||
theme: typebot.theme,
|
||||
variables: typebot.variables,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
|
||||
export const parsePublicTypebotToTypebot = (
|
||||
typebot: PublicTypebot,
|
||||
existingTypebot: Typebot
|
||||
): Typebot => ({
|
||||
id: typebot.typebotId,
|
||||
groups: typebot.groups,
|
||||
edges: typebot.edges,
|
||||
name: existingTypebot.name,
|
||||
publicId: existingTypebot.publicId,
|
||||
settings: typebot.settings,
|
||||
theme: typebot.theme,
|
||||
variables: typebot.variables,
|
||||
customDomain: existingTypebot.customDomain,
|
||||
createdAt: existingTypebot.createdAt,
|
||||
updatedAt: existingTypebot.updatedAt,
|
||||
publishedTypebotId: typebot.id,
|
||||
folderId: existingTypebot.folderId,
|
||||
icon: existingTypebot.icon,
|
||||
workspaceId: existingTypebot.workspaceId,
|
||||
isArchived: existingTypebot.isArchived,
|
||||
isClosed: existingTypebot.isClosed,
|
||||
})
|
||||
|
||||
export const createPublishedTypebot = async (
|
||||
typebot: PublicTypebot,
|
||||
workspaceId: string
|
||||
) =>
|
||||
sendRequest<PublicTypebot>({
|
||||
url: `/api/publicTypebots?workspaceId=${workspaceId}`,
|
||||
method: 'POST',
|
||||
body: typebot,
|
||||
})
|
||||
|
||||
export const updatePublishedTypebot = async (
|
||||
id: string,
|
||||
typebot: Omit<PublicTypebot, 'id'>,
|
||||
workspaceId: string
|
||||
) =>
|
||||
sendRequest({
|
||||
url: `/api/publicTypebots/${id}?workspaceId=${workspaceId}`,
|
||||
method: 'PUT',
|
||||
body: typebot,
|
||||
})
|
@ -1,48 +0,0 @@
|
||||
import { CollaboratorsOnTypebots } from 'db'
|
||||
import { fetcher } from 'services/utils'
|
||||
import useSWR from 'swr'
|
||||
import { sendRequest } from 'utils'
|
||||
|
||||
export type Collaborator = CollaboratorsOnTypebots & {
|
||||
user: {
|
||||
name: string | null
|
||||
image: string | null
|
||||
email: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export const useCollaborators = ({
|
||||
typebotId,
|
||||
onError,
|
||||
}: {
|
||||
typebotId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<
|
||||
{ collaborators: Collaborator[] },
|
||||
Error
|
||||
>(typebotId ? `/api/typebots/${typebotId}/collaborators` : null, fetcher)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
collaborators: data?.collaborators,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
export const updateCollaborator = (
|
||||
typebotId: string,
|
||||
userId: string,
|
||||
collaborator: CollaboratorsOnTypebots
|
||||
) =>
|
||||
sendRequest({
|
||||
method: 'PATCH',
|
||||
url: `/api/typebots/${typebotId}/collaborators/${userId}`,
|
||||
body: collaborator,
|
||||
})
|
||||
|
||||
export const deleteCollaborator = (typebotId: string, userId: string) =>
|
||||
sendRequest({
|
||||
method: 'DELETE',
|
||||
url: `/api/typebots/${typebotId}/collaborators/${userId}`,
|
||||
})
|
@ -1 +0,0 @@
|
||||
export * from './typebots'
|
@ -1,53 +0,0 @@
|
||||
import { CollaborationType, Invitation } from 'db'
|
||||
import { fetcher } from 'services/utils'
|
||||
import useSWR from 'swr'
|
||||
import { env, sendRequest } from 'utils'
|
||||
|
||||
export const useInvitations = ({
|
||||
typebotId,
|
||||
onError,
|
||||
}: {
|
||||
typebotId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<{ invitations: Invitation[] }, Error>(
|
||||
typebotId ? `/api/typebots/${typebotId}/invitations` : null,
|
||||
fetcher,
|
||||
{
|
||||
dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
|
||||
}
|
||||
)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
invitations: data?.invitations,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
export const sendInvitation = (
|
||||
typebotId: string,
|
||||
{ email, type }: { email: string; type: CollaborationType }
|
||||
) =>
|
||||
sendRequest({
|
||||
method: 'POST',
|
||||
url: `/api/typebots/${typebotId}/invitations`,
|
||||
body: { email, type },
|
||||
})
|
||||
|
||||
export const updateInvitation = (
|
||||
typebotId: string,
|
||||
email: string,
|
||||
invitation: Omit<Invitation, 'createdAt' | 'id'>
|
||||
) =>
|
||||
sendRequest({
|
||||
method: 'PATCH',
|
||||
url: `/api/typebots/${typebotId}/invitations/${email}`,
|
||||
body: invitation,
|
||||
})
|
||||
|
||||
export const deleteInvitation = (typebotId: string, email: string) =>
|
||||
sendRequest({
|
||||
method: 'DELETE',
|
||||
url: `/api/typebots/${typebotId}/invitations/${email}`,
|
||||
})
|
@ -1,225 +0,0 @@
|
||||
import {
|
||||
ResultWithAnswers,
|
||||
VariableWithValue,
|
||||
ResultHeaderCell,
|
||||
InputBlockType,
|
||||
} from 'models'
|
||||
import useSWRInfinite from 'swr/infinite'
|
||||
import { stringify } from 'qs'
|
||||
import { Answer } from 'db'
|
||||
import { env, isDefined, sendRequest } from 'utils'
|
||||
import { fetcher } from 'services/utils'
|
||||
import { HStack, Text, Wrap, WrapItem } from '@chakra-ui/react'
|
||||
import { CodeIcon, CalendarIcon, FileIcon } from 'assets/icons'
|
||||
import { Link } from '@chakra-ui/react'
|
||||
import { BlockIcon } from 'components/editor/BlocksSideBar/BlockIcon'
|
||||
|
||||
const paginationLimit = 50
|
||||
|
||||
const getKey = (
|
||||
workspaceId: string,
|
||||
typebotId: string,
|
||||
pageIndex: number,
|
||||
previousPageData: {
|
||||
results: ResultWithAnswers[]
|
||||
}
|
||||
) => {
|
||||
if (previousPageData && previousPageData.results.length === 0) return null
|
||||
if (pageIndex === 0)
|
||||
return `/api/typebots/${typebotId}/results?limit=50&workspaceId=${workspaceId}`
|
||||
return `/api/typebots/${typebotId}/results?lastResultId=${
|
||||
previousPageData.results[previousPageData.results.length - 1].id
|
||||
}&limit=${paginationLimit}&workspaceId=${workspaceId}`
|
||||
}
|
||||
|
||||
export const useResults = ({
|
||||
workspaceId,
|
||||
typebotId,
|
||||
onError,
|
||||
}: {
|
||||
workspaceId: string
|
||||
typebotId: string
|
||||
onError?: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate, setSize, size, isValidating } = useSWRInfinite<
|
||||
{ results: ResultWithAnswers[] },
|
||||
Error
|
||||
>(
|
||||
(
|
||||
pageIndex: number,
|
||||
previousPageData: {
|
||||
results: ResultWithAnswers[]
|
||||
}
|
||||
) => getKey(workspaceId, typebotId, pageIndex, previousPageData),
|
||||
fetcher,
|
||||
{
|
||||
revalidateAll: true,
|
||||
dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
|
||||
}
|
||||
)
|
||||
|
||||
if (error && onError) onError(error)
|
||||
return {
|
||||
data,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
setSize,
|
||||
size,
|
||||
hasMore:
|
||||
isValidating ||
|
||||
(data &&
|
||||
data.length > 0 &&
|
||||
data[data.length - 1].results.length > 0 &&
|
||||
data.length === paginationLimit),
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteResults = async (
|
||||
workspaceId: string,
|
||||
typebotId: string,
|
||||
ids: string[]
|
||||
) => {
|
||||
const params = stringify({
|
||||
workspaceId,
|
||||
})
|
||||
return sendRequest({
|
||||
url: `/api/typebots/${typebotId}/results?${params}`,
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
ids,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const getAllResults = async (workspaceId: string, typebotId: string) => {
|
||||
const results = []
|
||||
let hasMore = true
|
||||
let lastResultId: string | undefined = undefined
|
||||
do {
|
||||
const query = stringify({ limit: 200, lastResultId, workspaceId })
|
||||
const { data, error } = await sendRequest<{ results: ResultWithAnswers[] }>(
|
||||
{
|
||||
url: `/api/typebots/${typebotId}/results?${query}`,
|
||||
method: 'GET',
|
||||
}
|
||||
)
|
||||
if (error) {
|
||||
console.error(error)
|
||||
break
|
||||
}
|
||||
results.push(...(data?.results ?? []))
|
||||
lastResultId = results[results.length - 1]?.id as string | undefined
|
||||
if (data?.results.length === 0) hasMore = false
|
||||
} while (hasMore)
|
||||
return results
|
||||
}
|
||||
|
||||
export const parseDateToReadable = (dateStr: string): string => {
|
||||
const date = new Date(dateStr)
|
||||
return (
|
||||
date.toDateString().split(' ').slice(1, 3).join(' ') +
|
||||
', ' +
|
||||
date.toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
type HeaderCell = {
|
||||
Header: JSX.Element
|
||||
accessor: string
|
||||
}
|
||||
|
||||
export const parseSubmissionsColumns = (
|
||||
resultHeader: ResultHeaderCell[]
|
||||
): HeaderCell[] =>
|
||||
resultHeader.map((header) => ({
|
||||
Header: (
|
||||
<HStack minW="150px" maxW="500px">
|
||||
<HeaderIcon header={header} />
|
||||
<Text>{header.label}</Text>
|
||||
</HStack>
|
||||
),
|
||||
accessor: header.label,
|
||||
}))
|
||||
|
||||
export const HeaderIcon = ({ header }: { header: ResultHeaderCell }) =>
|
||||
header.blockType ? (
|
||||
<BlockIcon type={header.blockType} />
|
||||
) : header.variableIds ? (
|
||||
<CodeIcon />
|
||||
) : (
|
||||
<CalendarIcon />
|
||||
)
|
||||
|
||||
export type CellValueType = { element?: JSX.Element; plainText: string }
|
||||
|
||||
export type TableData = {
|
||||
id: Pick<CellValueType, 'plainText'>
|
||||
} & Record<string, CellValueType>
|
||||
|
||||
export const convertResultsToTableData = (
|
||||
results: ResultWithAnswers[] | undefined,
|
||||
headerCells: ResultHeaderCell[]
|
||||
): TableData[] =>
|
||||
(results ?? []).map((result) => ({
|
||||
id: { plainText: result.id },
|
||||
'Submitted at': {
|
||||
plainText: parseDateToReadable(result.createdAt),
|
||||
},
|
||||
...[...result.answers, ...result.variables].reduce<{
|
||||
[key: string]: { element?: JSX.Element; plainText: string }
|
||||
}>((o, answerOrVariable) => {
|
||||
if ('groupId' in answerOrVariable) {
|
||||
const answer = answerOrVariable as Answer
|
||||
const header = answer.variableId
|
||||
? headerCells.find((headerCell) =>
|
||||
headerCell.variableIds?.includes(answer.variableId as string)
|
||||
)
|
||||
: headerCells.find((headerCell) =>
|
||||
headerCell.blocks?.some((block) => block.id === answer.blockId)
|
||||
)
|
||||
if (!header || !header.blocks || !header.blockType) return o
|
||||
return {
|
||||
...o,
|
||||
[header.label]: {
|
||||
element: parseContent(answer.content, header.blockType),
|
||||
plainText: answer.content,
|
||||
},
|
||||
}
|
||||
}
|
||||
const variable = answerOrVariable as VariableWithValue
|
||||
const key = headerCells.find((headerCell) =>
|
||||
headerCell.variableIds?.includes(variable.id)
|
||||
)?.label
|
||||
if (!key) return o
|
||||
if (isDefined(o[key])) return o
|
||||
return {
|
||||
...o,
|
||||
[key]: { plainText: variable.value?.toString() },
|
||||
}
|
||||
}, {}),
|
||||
}))
|
||||
|
||||
const parseContent = (
|
||||
str: string,
|
||||
blockType: InputBlockType
|
||||
): JSX.Element | undefined =>
|
||||
blockType === InputBlockType.FILE ? parseFileContent(str) : undefined
|
||||
|
||||
const parseFileContent = (str: string) => {
|
||||
const fileNames = str.split(', ')
|
||||
return (
|
||||
<Wrap maxW="300px">
|
||||
{fileNames.map((name) => (
|
||||
<HStack as={WrapItem} key={name}>
|
||||
<FileIcon />
|
||||
<Link href={name} isExternal color="blue.500">
|
||||
{name.split('/').pop()}
|
||||
</Link>
|
||||
</HStack>
|
||||
))}
|
||||
</Wrap>
|
||||
)
|
||||
}
|
@ -1,463 +0,0 @@
|
||||
import {
|
||||
Group,
|
||||
PublicTypebot,
|
||||
StartBlock,
|
||||
BubbleBlockType,
|
||||
InputBlockType,
|
||||
LogicBlockType,
|
||||
Block,
|
||||
DraggableBlockType,
|
||||
DraggableBlock,
|
||||
defaultTheme,
|
||||
defaultSettings,
|
||||
BlockOptions,
|
||||
BubbleBlockContent,
|
||||
IntegrationBlockType,
|
||||
defaultTextBubbleContent,
|
||||
defaultImageBubbleContent,
|
||||
defaultVideoBubbleContent,
|
||||
defaultTextInputOptions,
|
||||
defaultNumberInputOptions,
|
||||
defaultEmailInputOptions,
|
||||
defaultDateInputOptions,
|
||||
defaultPhoneInputOptions,
|
||||
defaultUrlInputOptions,
|
||||
defaultChoiceInputOptions,
|
||||
defaultSetVariablesOptions,
|
||||
defaultRedirectOptions,
|
||||
defaultGoogleSheetsOptions,
|
||||
defaultGoogleAnalyticsOptions,
|
||||
defaultCodeOptions,
|
||||
defaultWebhookOptions,
|
||||
BlockWithOptionsType,
|
||||
Item,
|
||||
ItemType,
|
||||
defaultConditionContent,
|
||||
defaultSendEmailOptions,
|
||||
defaultEmbedBubbleContent,
|
||||
ChoiceInputBlock,
|
||||
ConditionBlock,
|
||||
defaultPaymentInputOptions,
|
||||
defaultRatingInputOptions,
|
||||
defaultFileInputOptions,
|
||||
} from 'models'
|
||||
import { Typebot } from 'models'
|
||||
import useSWR from 'swr'
|
||||
import { fetcher, toKebabCase } from '../utils'
|
||||
import {
|
||||
isBubbleBlockType,
|
||||
isWebhookBlock,
|
||||
omit,
|
||||
blockHasItems,
|
||||
blockTypeHasItems,
|
||||
blockTypeHasOption,
|
||||
blockTypeHasWebhook,
|
||||
env,
|
||||
isDefined,
|
||||
} from 'utils'
|
||||
import { dequal } from 'dequal'
|
||||
import { stringify } from 'qs'
|
||||
import { isChoiceInput, isConditionBlock, sendRequest } from 'utils'
|
||||
import cuid from 'cuid'
|
||||
import { diff } from 'deep-object-diff'
|
||||
import { duplicateWebhook } from 'services/webhook'
|
||||
import { Plan } from 'db'
|
||||
import { defaultChatwootOptions } from 'models'
|
||||
|
||||
export type TypebotInDashboard = Pick<
|
||||
Typebot,
|
||||
'id' | 'name' | 'publishedTypebotId' | 'icon'
|
||||
>
|
||||
export const useTypebots = ({
|
||||
folderId,
|
||||
workspaceId,
|
||||
allFolders,
|
||||
onError,
|
||||
}: {
|
||||
workspaceId?: string
|
||||
folderId?: string
|
||||
allFolders?: boolean
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const params = stringify({ folderId, allFolders, workspaceId })
|
||||
const { data, error, mutate } = useSWR<
|
||||
{ typebots: TypebotInDashboard[] },
|
||||
Error
|
||||
>(workspaceId ? `/api/typebots?${params}` : null, fetcher, {
|
||||
dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
|
||||
})
|
||||
if (error) onError(error)
|
||||
return {
|
||||
typebots: data?.typebots,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
export const createTypebot = async ({
|
||||
folderId,
|
||||
workspaceId,
|
||||
}: Pick<Typebot, 'folderId' | 'workspaceId'>) => {
|
||||
const typebot = {
|
||||
folderId,
|
||||
name: 'My typebot',
|
||||
workspaceId,
|
||||
}
|
||||
return sendRequest<Typebot>({
|
||||
url: `/api/typebots`,
|
||||
method: 'POST',
|
||||
body: typebot,
|
||||
})
|
||||
}
|
||||
|
||||
export const importTypebot = async (typebot: Typebot, userPlan: Plan) => {
|
||||
const { typebot: newTypebot, webhookIdsMapping } = duplicateTypebot(
|
||||
typebot,
|
||||
userPlan
|
||||
)
|
||||
const { data, error } = await sendRequest<Typebot>({
|
||||
url: `/api/typebots`,
|
||||
method: 'POST',
|
||||
body: newTypebot,
|
||||
})
|
||||
if (!data) return { data, error }
|
||||
const webhookBlocks = typebot.groups
|
||||
.flatMap((b) => b.blocks)
|
||||
.filter(isWebhookBlock)
|
||||
await Promise.all(
|
||||
webhookBlocks.map((s) =>
|
||||
duplicateWebhook(
|
||||
newTypebot.id,
|
||||
s.webhookId,
|
||||
webhookIdsMapping.get(s.webhookId) as string
|
||||
)
|
||||
)
|
||||
)
|
||||
return { data, error }
|
||||
}
|
||||
|
||||
const duplicateTypebot = (
|
||||
typebot: Typebot,
|
||||
userPlan: Plan
|
||||
): { typebot: Typebot; webhookIdsMapping: Map<string, string> } => {
|
||||
const groupIdsMapping = generateOldNewIdsMapping(typebot.groups)
|
||||
const edgeIdsMapping = generateOldNewIdsMapping(typebot.edges)
|
||||
const webhookIdsMapping = generateOldNewIdsMapping(
|
||||
typebot.groups
|
||||
.flatMap((b) => b.blocks)
|
||||
.filter(isWebhookBlock)
|
||||
.map((s) => ({ id: s.webhookId }))
|
||||
)
|
||||
const id = cuid()
|
||||
return {
|
||||
typebot: {
|
||||
...typebot,
|
||||
id,
|
||||
name: `${typebot.name} copy`,
|
||||
publishedTypebotId: null,
|
||||
publicId: null,
|
||||
customDomain: null,
|
||||
groups: typebot.groups.map((b) => ({
|
||||
...b,
|
||||
id: groupIdsMapping.get(b.id) as string,
|
||||
blocks: b.blocks.map((s) => {
|
||||
const newIds = {
|
||||
groupId: groupIdsMapping.get(s.groupId) as string,
|
||||
outgoingEdgeId: s.outgoingEdgeId
|
||||
? edgeIdsMapping.get(s.outgoingEdgeId)
|
||||
: undefined,
|
||||
}
|
||||
if (
|
||||
s.type === LogicBlockType.TYPEBOT_LINK &&
|
||||
s.options.typebotId === 'current' &&
|
||||
isDefined(s.options.groupId)
|
||||
)
|
||||
return {
|
||||
...s,
|
||||
options: {
|
||||
...s.options,
|
||||
groupId: groupIdsMapping.get(s.options.groupId as string),
|
||||
},
|
||||
}
|
||||
if (blockHasItems(s))
|
||||
return {
|
||||
...s,
|
||||
items: s.items.map((item) => ({
|
||||
...item,
|
||||
outgoingEdgeId: item.outgoingEdgeId
|
||||
? (edgeIdsMapping.get(item.outgoingEdgeId) as string)
|
||||
: undefined,
|
||||
})),
|
||||
...newIds,
|
||||
} as ChoiceInputBlock | ConditionBlock
|
||||
if (isWebhookBlock(s)) {
|
||||
return {
|
||||
...s,
|
||||
webhookId: webhookIdsMapping.get(s.webhookId) as string,
|
||||
...newIds,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...s,
|
||||
...newIds,
|
||||
}
|
||||
}),
|
||||
})),
|
||||
edges: typebot.edges.map((e) => ({
|
||||
...e,
|
||||
id: edgeIdsMapping.get(e.id) as string,
|
||||
from: {
|
||||
...e.from,
|
||||
groupId: groupIdsMapping.get(e.from.groupId) as string,
|
||||
},
|
||||
to: { ...e.to, groupId: groupIdsMapping.get(e.to.groupId) as string },
|
||||
})),
|
||||
settings:
|
||||
typebot.settings.general.isBrandingEnabled === false &&
|
||||
userPlan === Plan.FREE
|
||||
? {
|
||||
...typebot.settings,
|
||||
general: { ...typebot.settings.general, isBrandingEnabled: true },
|
||||
}
|
||||
: typebot.settings,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
resultsTablePreferences: typebot.resultsTablePreferences ?? undefined,
|
||||
},
|
||||
webhookIdsMapping,
|
||||
}
|
||||
}
|
||||
|
||||
const generateOldNewIdsMapping = (itemWithId: { id: string }[]) => {
|
||||
const idsMapping: Map<string, string> = new Map()
|
||||
itemWithId.forEach((item) => idsMapping.set(item.id, cuid()))
|
||||
return idsMapping
|
||||
}
|
||||
|
||||
export const getTypebot = (typebotId: string) =>
|
||||
sendRequest<{ typebot: Typebot }>({
|
||||
url: `/api/typebots/${typebotId}`,
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
export const deleteTypebot = async (id: string) =>
|
||||
sendRequest({
|
||||
url: `/api/typebots/${id}`,
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
export const updateTypebot = async (id: string, typebot: Typebot) =>
|
||||
sendRequest({
|
||||
url: `/api/typebots/${id}`,
|
||||
method: 'PUT',
|
||||
body: typebot,
|
||||
})
|
||||
|
||||
export const patchTypebot = async (id: string, typebot: Partial<Typebot>) =>
|
||||
sendRequest({
|
||||
url: `/api/typebots/${id}`,
|
||||
method: 'PATCH',
|
||||
body: typebot,
|
||||
})
|
||||
|
||||
export const parseNewBlock = (
|
||||
type: DraggableBlockType,
|
||||
groupId: string
|
||||
): DraggableBlock => {
|
||||
const id = cuid()
|
||||
return {
|
||||
id,
|
||||
groupId,
|
||||
type,
|
||||
content: isBubbleBlockType(type) ? parseDefaultContent(type) : undefined,
|
||||
options: blockTypeHasOption(type)
|
||||
? parseDefaultBlockOptions(type)
|
||||
: undefined,
|
||||
webhookId: blockTypeHasWebhook(type) ? cuid() : undefined,
|
||||
items: blockTypeHasItems(type) ? parseDefaultItems(type, id) : undefined,
|
||||
} as DraggableBlock
|
||||
}
|
||||
|
||||
const parseDefaultItems = (
|
||||
type: LogicBlockType.CONDITION | InputBlockType.CHOICE,
|
||||
blockId: string
|
||||
): Item[] => {
|
||||
switch (type) {
|
||||
case InputBlockType.CHOICE:
|
||||
return [{ id: cuid(), blockId, type: ItemType.BUTTON }]
|
||||
case LogicBlockType.CONDITION:
|
||||
return [
|
||||
{
|
||||
id: cuid(),
|
||||
blockId,
|
||||
type: ItemType.CONDITION,
|
||||
content: defaultConditionContent,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const parseDefaultContent = (type: BubbleBlockType): BubbleBlockContent => {
|
||||
switch (type) {
|
||||
case BubbleBlockType.TEXT:
|
||||
return defaultTextBubbleContent
|
||||
case BubbleBlockType.IMAGE:
|
||||
return defaultImageBubbleContent
|
||||
case BubbleBlockType.VIDEO:
|
||||
return defaultVideoBubbleContent
|
||||
case BubbleBlockType.EMBED:
|
||||
return defaultEmbedBubbleContent
|
||||
}
|
||||
}
|
||||
|
||||
const parseDefaultBlockOptions = (type: BlockWithOptionsType): BlockOptions => {
|
||||
switch (type) {
|
||||
case InputBlockType.TEXT:
|
||||
return defaultTextInputOptions
|
||||
case InputBlockType.NUMBER:
|
||||
return defaultNumberInputOptions
|
||||
case InputBlockType.EMAIL:
|
||||
return defaultEmailInputOptions
|
||||
case InputBlockType.DATE:
|
||||
return defaultDateInputOptions
|
||||
case InputBlockType.PHONE:
|
||||
return defaultPhoneInputOptions
|
||||
case InputBlockType.URL:
|
||||
return defaultUrlInputOptions
|
||||
case InputBlockType.CHOICE:
|
||||
return defaultChoiceInputOptions
|
||||
case InputBlockType.PAYMENT:
|
||||
return defaultPaymentInputOptions
|
||||
case InputBlockType.RATING:
|
||||
return defaultRatingInputOptions
|
||||
case InputBlockType.FILE:
|
||||
return defaultFileInputOptions
|
||||
case LogicBlockType.SET_VARIABLE:
|
||||
return defaultSetVariablesOptions
|
||||
case LogicBlockType.REDIRECT:
|
||||
return defaultRedirectOptions
|
||||
case LogicBlockType.CODE:
|
||||
return defaultCodeOptions
|
||||
case LogicBlockType.TYPEBOT_LINK:
|
||||
return {}
|
||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||
return defaultGoogleSheetsOptions
|
||||
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
||||
return defaultGoogleAnalyticsOptions
|
||||
case IntegrationBlockType.ZAPIER:
|
||||
case IntegrationBlockType.PABBLY_CONNECT:
|
||||
case IntegrationBlockType.MAKE_COM:
|
||||
case IntegrationBlockType.WEBHOOK:
|
||||
return defaultWebhookOptions
|
||||
case IntegrationBlockType.EMAIL:
|
||||
return defaultSendEmailOptions
|
||||
case IntegrationBlockType.CHATWOOT:
|
||||
return defaultChatwootOptions
|
||||
}
|
||||
}
|
||||
|
||||
export const checkIfTypebotsAreEqual = (typebotA: Typebot, typebotB: Typebot) =>
|
||||
dequal(
|
||||
JSON.parse(JSON.stringify(omit(typebotA, 'updatedAt'))),
|
||||
JSON.parse(JSON.stringify(omit(typebotB, 'updatedAt')))
|
||||
)
|
||||
|
||||
export const checkIfPublished = (
|
||||
typebot: Typebot,
|
||||
publicTypebot: PublicTypebot,
|
||||
debug?: boolean
|
||||
) => {
|
||||
if (debug)
|
||||
console.log(
|
||||
diff(
|
||||
JSON.parse(JSON.stringify(typebot.groups)),
|
||||
JSON.parse(JSON.stringify(publicTypebot.groups))
|
||||
)
|
||||
)
|
||||
return (
|
||||
dequal(
|
||||
JSON.parse(JSON.stringify(typebot.groups)),
|
||||
JSON.parse(JSON.stringify(publicTypebot.groups))
|
||||
) &&
|
||||
dequal(
|
||||
JSON.parse(JSON.stringify(typebot.settings)),
|
||||
JSON.parse(JSON.stringify(publicTypebot.settings))
|
||||
) &&
|
||||
dequal(
|
||||
JSON.parse(JSON.stringify(typebot.theme)),
|
||||
JSON.parse(JSON.stringify(publicTypebot.theme))
|
||||
) &&
|
||||
dequal(
|
||||
JSON.parse(JSON.stringify(typebot.variables)),
|
||||
JSON.parse(JSON.stringify(publicTypebot.variables))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export const parseDefaultPublicId = (name: string, id: string) =>
|
||||
toKebabCase(name) + `-${id?.slice(-7)}`
|
||||
|
||||
export const parseNewTypebot = ({
|
||||
folderId,
|
||||
name,
|
||||
ownerAvatarUrl,
|
||||
workspaceId,
|
||||
isBrandingEnabled = true,
|
||||
}: {
|
||||
folderId: string | null
|
||||
workspaceId: string
|
||||
name: string
|
||||
ownerAvatarUrl?: string
|
||||
isBrandingEnabled?: boolean
|
||||
}): Omit<
|
||||
Typebot,
|
||||
| 'createdAt'
|
||||
| 'updatedAt'
|
||||
| 'id'
|
||||
| 'publishedTypebotId'
|
||||
| 'publicId'
|
||||
| 'customDomain'
|
||||
| 'icon'
|
||||
| 'isArchived'
|
||||
| 'isClosed'
|
||||
> => {
|
||||
const startGroupId = cuid()
|
||||
const startBlockId = cuid()
|
||||
const startBlock: StartBlock = {
|
||||
groupId: startGroupId,
|
||||
id: startBlockId,
|
||||
label: 'Start',
|
||||
type: 'start',
|
||||
}
|
||||
const startGroup: Group = {
|
||||
id: startGroupId,
|
||||
title: 'Start',
|
||||
graphCoordinates: { x: 0, y: 0 },
|
||||
blocks: [startBlock],
|
||||
}
|
||||
return {
|
||||
folderId,
|
||||
name,
|
||||
workspaceId,
|
||||
groups: [startGroup],
|
||||
edges: [],
|
||||
variables: [],
|
||||
theme: {
|
||||
...defaultTheme,
|
||||
chat: {
|
||||
...defaultTheme.chat,
|
||||
hostAvatar: { isEnabled: true, url: ownerAvatarUrl },
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
...defaultSettings,
|
||||
general: {
|
||||
...defaultSettings.general,
|
||||
isBrandingEnabled,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const hasDefaultConnector = (block: Block) =>
|
||||
!isChoiceInput(block) && !isConditionBlock(block)
|
@ -1,58 +0,0 @@
|
||||
import { ApiToken } from 'db'
|
||||
import { fetcher } from 'services/utils'
|
||||
import useSWR, { KeyedMutator } from 'swr'
|
||||
import { env, sendRequest } from 'utils'
|
||||
|
||||
export type ApiTokenFromServer = { id: string; name: string; createdAt: string }
|
||||
|
||||
type ReturnedProps = {
|
||||
apiTokens?: ApiTokenFromServer[]
|
||||
isLoading: boolean
|
||||
mutate: KeyedMutator<ServerResponse>
|
||||
}
|
||||
|
||||
type ServerResponse = {
|
||||
apiTokens: ApiTokenFromServer[]
|
||||
}
|
||||
export const useApiTokens = ({
|
||||
userId,
|
||||
onError,
|
||||
}: {
|
||||
userId?: string
|
||||
onError: (error: Error) => void
|
||||
}): ReturnedProps => {
|
||||
const { data, error, mutate } = useSWR<ServerResponse, Error>(
|
||||
userId ? `/api/users/${userId}/api-tokens` : null,
|
||||
fetcher,
|
||||
{
|
||||
dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
|
||||
}
|
||||
)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
apiTokens: data?.apiTokens,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
export const createApiToken = (userId: string, { name }: { name: string }) =>
|
||||
sendRequest<{ apiToken: ApiTokenFromServer & { token: string } }>({
|
||||
url: `/api/users/${userId}/api-tokens`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
name,
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteApiToken = ({
|
||||
userId,
|
||||
tokenId,
|
||||
}: {
|
||||
userId: string
|
||||
tokenId: string
|
||||
}) =>
|
||||
sendRequest<{ apiToken: ApiToken }>({
|
||||
url: `/api/users/${userId}/api-tokens/${tokenId}`,
|
||||
method: 'DELETE',
|
||||
})
|
@ -1,3 +0,0 @@
|
||||
export * from './user'
|
||||
export * from '../customDomains'
|
||||
export * from '../credentials'
|
@ -1,3 +0,0 @@
|
||||
export * from './utils'
|
||||
export * from './useUndo'
|
||||
export * from './useRefState'
|
@ -1,10 +0,0 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
export const useRefState = (initialValue: string) => {
|
||||
const [state, setState] = useState(initialValue)
|
||||
const stateRef = useRef<string>(state)
|
||||
useEffect(() => {
|
||||
stateRef.current = state
|
||||
}, [state])
|
||||
return [stateRef, setState]
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export * from './workspace'
|
||||
export * from './member'
|
||||
export * from './invitation'
|
@ -1,28 +0,0 @@
|
||||
import { WorkspaceInvitation } from 'db'
|
||||
import { sendRequest } from 'utils'
|
||||
import { Member } from './member'
|
||||
|
||||
export const sendInvitation = (
|
||||
invitation: Omit<WorkspaceInvitation, 'id' | 'createdAt'>
|
||||
) =>
|
||||
sendRequest<{ invitation?: WorkspaceInvitation; member?: Member }>({
|
||||
url: `/api/workspaces/${invitation.workspaceId}/invitations`,
|
||||
method: 'POST',
|
||||
body: invitation,
|
||||
})
|
||||
|
||||
export const updateInvitation = (invitation: Partial<WorkspaceInvitation>) =>
|
||||
sendRequest({
|
||||
url: `/api/workspaces/${invitation.workspaceId}/invitations/${invitation.id}`,
|
||||
method: 'PATCH',
|
||||
body: invitation,
|
||||
})
|
||||
|
||||
export const deleteInvitation = (invitation: {
|
||||
workspaceId: string
|
||||
id: string
|
||||
}) =>
|
||||
sendRequest({
|
||||
url: `/api/workspaces/${invitation.workspaceId}/invitations/${invitation.id}`,
|
||||
method: 'DELETE',
|
||||
})
|
@ -1,41 +0,0 @@
|
||||
import { MemberInWorkspace, WorkspaceInvitation } from 'db'
|
||||
import { fetcher } from 'services/utils'
|
||||
import useSWR from 'swr'
|
||||
import { env, sendRequest } from 'utils'
|
||||
|
||||
export type Member = MemberInWorkspace & {
|
||||
name: string | null
|
||||
image: string | null
|
||||
email: string | null
|
||||
}
|
||||
|
||||
export const useMembers = ({ workspaceId }: { workspaceId?: string }) => {
|
||||
const { data, error, mutate } = useSWR<
|
||||
{ members: Member[]; invitations: WorkspaceInvitation[] },
|
||||
Error
|
||||
>(workspaceId ? `/api/workspaces/${workspaceId}/members` : null, fetcher, {
|
||||
dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
|
||||
})
|
||||
return {
|
||||
members: data?.members ?? [],
|
||||
invitations: data?.invitations ?? [],
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
export const updateMember = (
|
||||
workspaceId: string,
|
||||
member: Partial<MemberInWorkspace>
|
||||
) =>
|
||||
sendRequest({
|
||||
method: 'PATCH',
|
||||
url: `/api/workspaces/${workspaceId}/members/${member.userId}`,
|
||||
body: member,
|
||||
})
|
||||
|
||||
export const deleteMember = (workspaceId: string, userId: string) =>
|
||||
sendRequest({
|
||||
method: 'DELETE',
|
||||
url: `/api/workspaces/${workspaceId}/members/${userId}`,
|
||||
})
|
@ -1,85 +0,0 @@
|
||||
import { WorkspaceWithMembers } from 'contexts/WorkspaceContext'
|
||||
import { Plan, Workspace } from 'db'
|
||||
import useSWR from 'swr'
|
||||
import { isDefined, isNotDefined, sendRequest } from 'utils'
|
||||
import { fetcher } from '../utils'
|
||||
|
||||
export const useWorkspaces = ({ userId }: { userId?: string }) => {
|
||||
const { data, error, mutate } = useSWR<
|
||||
{
|
||||
workspaces: WorkspaceWithMembers[]
|
||||
},
|
||||
Error
|
||||
>(userId ? `/api/workspaces` : null, fetcher)
|
||||
return {
|
||||
workspaces: data?.workspaces,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
export const createNewWorkspace = async (
|
||||
body: Omit<
|
||||
Workspace,
|
||||
| 'id'
|
||||
| 'icon'
|
||||
| 'createdAt'
|
||||
| 'stripeId'
|
||||
| 'additionalChatsIndex'
|
||||
| 'additionalStorageIndex'
|
||||
| 'chatsLimitFirstEmailSentAt'
|
||||
| 'chatsLimitSecondEmailSentAt'
|
||||
| 'storageLimitFirstEmailSentAt'
|
||||
| 'storageLimitSecondEmailSentAt'
|
||||
| 'customChatsLimit'
|
||||
| 'customStorageLimit'
|
||||
| 'customSeatsLimit'
|
||||
>
|
||||
) =>
|
||||
sendRequest<{
|
||||
workspace: Workspace
|
||||
}>({
|
||||
url: `/api/workspaces`,
|
||||
method: 'POST',
|
||||
body,
|
||||
})
|
||||
|
||||
export const updateWorkspace = async (updates: Partial<Workspace>) =>
|
||||
sendRequest<{
|
||||
workspace: Workspace
|
||||
}>({
|
||||
url: `/api/workspaces/${updates.id}`,
|
||||
method: 'PATCH',
|
||||
body: updates,
|
||||
})
|
||||
|
||||
export const deleteWorkspace = (workspaceId: string) =>
|
||||
sendRequest<{
|
||||
workspace: Workspace
|
||||
}>({
|
||||
url: `/api/workspaces/${workspaceId}`,
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
export const planToReadable = (plan?: Plan) => {
|
||||
if (!plan) return
|
||||
switch (plan) {
|
||||
case Plan.FREE:
|
||||
return 'Free'
|
||||
case Plan.LIFETIME:
|
||||
return 'Lifetime'
|
||||
case Plan.OFFERED:
|
||||
return 'Offered'
|
||||
case Plan.PRO:
|
||||
return 'Pro'
|
||||
}
|
||||
}
|
||||
|
||||
export const isFreePlan = (workspace?: Pick<Workspace, 'plan'>) =>
|
||||
isNotDefined(workspace) || workspace?.plan === Plan.FREE
|
||||
|
||||
export const isWorkspaceProPlan = (workspace?: Pick<Workspace, 'plan'>) =>
|
||||
isDefined(workspace) &&
|
||||
(workspace.plan === Plan.PRO ||
|
||||
workspace.plan === Plan.LIFETIME ||
|
||||
workspace.plan === Plan.CUSTOM)
|
8
apps/builder/src/components/AlertInfo.tsx
Normal file
8
apps/builder/src/components/AlertInfo.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import { AlertProps, Alert, AlertIcon } from '@chakra-ui/react'
|
||||
|
||||
export const AlertInfo = (props: AlertProps) => (
|
||||
<Alert status="info" bgColor={'blue.50'} rounded="md" {...props}>
|
||||
<AlertIcon />
|
||||
{props.children}
|
||||
</Alert>
|
||||
)
|
@ -8,7 +8,7 @@ import { html } from '@codemirror/lang-html'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useDebouncedCallback } from 'use-debounce'
|
||||
import { linter, LintSource } from '@codemirror/lint'
|
||||
import { VariablesButton } from './buttons/VariablesButton'
|
||||
import { VariablesButton } from '@/features/variables'
|
||||
import { Variable } from 'models'
|
||||
import { env } from 'utils'
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
Portal,
|
||||
Stack,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon } from 'assets/icons'
|
||||
import { ChevronLeftIcon } from '@/components/icons'
|
||||
import React, { ReactNode } from 'react'
|
||||
|
||||
type Props<T> = {
|
@ -1,4 +1,4 @@
|
||||
import { ToolIcon } from 'assets/icons'
|
||||
import { ToolIcon } from '@/components/icons'
|
||||
import React from 'react'
|
||||
import { chakra, IconProps, Image } from '@chakra-ui/react'
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Flex, Stack, Text } from '@chakra-ui/react'
|
||||
import { GiphyFetch } from '@giphy/js-fetch-api'
|
||||
import { Grid } from '@giphy/react-components'
|
||||
import { GiphyLogo } from 'assets/logos'
|
||||
import { GiphyLogo } from './GiphyLogo'
|
||||
import React, { useState } from 'react'
|
||||
import { env, isEmpty } from 'utils'
|
||||
import { Input } from '../Textbox'
|
||||
import { Input } from '../inputs'
|
||||
|
||||
type GiphySearchFormProps = {
|
||||
onSubmit: (url: string) => void
|
@ -1,8 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import { Button, Flex, HStack, Stack } from '@chakra-ui/react'
|
||||
import { UploadButton } from '../buttons/UploadButton'
|
||||
import { UploadButton } from './UploadButton'
|
||||
import { GiphySearchForm } from './GiphySearchForm'
|
||||
import { Input } from '../Textbox/Input'
|
||||
import { Input } from '../inputs/Input'
|
||||
import { EmojiSearchableList } from './emoji/EmojiSearchableList'
|
||||
|
||||
type Props = {
|
@ -1,6 +1,6 @@
|
||||
import { compressFile } from '@/utils/helpers'
|
||||
import { Button, ButtonProps, chakra } from '@chakra-ui/react'
|
||||
import React, { ChangeEvent, useState } from 'react'
|
||||
import { compressFile } from 'services/utils'
|
||||
import { ChangeEvent, useState } from 'react'
|
||||
import { uploadFiles } from 'utils'
|
||||
|
||||
type UploadButtonProps = {
|
@ -1,6 +1,5 @@
|
||||
import { Tooltip, chakra } from '@chakra-ui/react'
|
||||
import { HelpCircleIcon } from 'assets/icons'
|
||||
import React from 'react'
|
||||
import { HelpCircleIcon } from './icons'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
@ -14,7 +14,7 @@ import { Variable } from 'models'
|
||||
import { useState, useRef, useEffect, ChangeEvent } from 'react'
|
||||
import { useDebouncedCallback } from 'use-debounce'
|
||||
import { env, isDefined } from 'utils'
|
||||
import { VariablesButton } from './buttons/VariablesButton'
|
||||
import { VariablesButton } from '@/features/variables'
|
||||
|
||||
type Props = {
|
||||
selectedItem?: string
|
@ -1,10 +1,10 @@
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { useUser } from 'contexts/UserContext'
|
||||
import { useWorkspace } from 'contexts/WorkspaceContext'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { useUser } from '@/features/account'
|
||||
import { useWorkspace } from '@/features/workspace'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { isCloudProdInstance } from 'services/utils'
|
||||
import { planToReadable } from 'services/workspace'
|
||||
import { initBubble } from 'typebot-js'
|
||||
import { isCloudProdInstance } from '@/utils/helpers'
|
||||
import { planToReadable } from '@/features/billing'
|
||||
|
||||
export const SupportBubble = () => {
|
||||
const { typebot } = useTypebot()
|
@ -1,5 +1,5 @@
|
||||
import { Box, Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react'
|
||||
import { TrashIcon, PlusIcon } from 'assets/icons'
|
||||
import { TrashIcon, PlusIcon } from '@/components/icons'
|
||||
import cuid from 'cuid'
|
||||
import React, { useState } from 'react'
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Link, { LinkProps } from 'next/link'
|
||||
import React from 'react'
|
||||
import { chakra, HStack, TextProps } from '@chakra-ui/react'
|
||||
import { ExternalLinkIcon } from 'assets/icons'
|
||||
import { ExternalLinkIcon } from '@/components/icons'
|
||||
|
||||
type TextLinkProps = LinkProps & TextProps & { isExternal?: boolean }
|
||||
|
@ -8,21 +8,9 @@ import {
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import { ChangePlanModal } from './modals/ChangePlanModal'
|
||||
import { LimitReached } from './modals/ChangePlanModal'
|
||||
import { ChangePlanModal, LimitReached } from '@/features/billing'
|
||||
|
||||
export const Info = (props: AlertProps) => (
|
||||
<Alert status="info" bgColor={'blue.50'} rounded="md" {...props}>
|
||||
<AlertIcon />
|
||||
{props.children}
|
||||
</Alert>
|
||||
)
|
||||
|
||||
export const PublishFirstInfo = (props: AlertProps) => (
|
||||
<Info {...props}>You need to publish your typebot first</Info>
|
||||
)
|
||||
|
||||
export const UnlockPlanInfo = ({
|
||||
export const UnlockPlanAlertInfo = ({
|
||||
contentLabel,
|
||||
buttonLabel = 'More info',
|
||||
type,
|
@ -11,8 +11,8 @@ import {
|
||||
IconButton,
|
||||
HStack,
|
||||
} from '@chakra-ui/react'
|
||||
import { EditIcon, PlusIcon, TrashIcon } from 'assets/icons'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { EditIcon, PlusIcon, TrashIcon } from '@/components/icons'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import cuid from 'cuid'
|
||||
import { Variable } from 'models'
|
||||
import React, { useState, useRef, ChangeEvent, useEffect } from 'react'
|
@ -10,7 +10,7 @@ import { Variable } from 'models'
|
||||
import React, { ChangeEvent, useEffect, useRef, useState } from 'react'
|
||||
import { useDebouncedCallback } from 'use-debounce'
|
||||
import { env } from 'utils'
|
||||
import { VariablesButton } from '../buttons/VariablesButton'
|
||||
import { VariablesButton } from '../../features/variables/components/VariablesButton'
|
||||
import { MoreInfoTooltip } from '../MoreInfoTooltip'
|
||||
|
||||
export type TextBoxProps = {
|
@ -1,2 +1,3 @@
|
||||
export { Input } from './Input'
|
||||
export { Textarea } from './Textarea'
|
||||
export { SmartNumberInput } from './SmartNumberInput'
|
@ -9,11 +9,11 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { isDefined, isNotDefined } from 'utils'
|
||||
import { updateUser as updateUserInDb } from 'services/user/user'
|
||||
import { dequal } from 'dequal'
|
||||
import { User } from 'db'
|
||||
import { setUser as setSentryUser } from '@sentry/nextjs'
|
||||
import { useToast } from 'components/shared/hooks/useToast'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { updateUserQuery } from './queries/updateUserQuery'
|
||||
|
||||
const userContext = createContext<{
|
||||
user?: User
|
||||
@ -28,7 +28,7 @@ const userContext = createContext<{
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
|
||||
export const UserContext = ({ children }: { children: ReactNode }) => {
|
||||
export const UserProvider = ({ children }: { children: ReactNode }) => {
|
||||
const router = useRouter()
|
||||
const { data: session, status } = useSession()
|
||||
const [user, setUser] = useState<User | undefined>()
|
||||
@ -84,7 +84,7 @@ export const UserContext = ({ children }: { children: ReactNode }) => {
|
||||
if (isNotDefined(user)) return
|
||||
setIsSaving(true)
|
||||
if (newUser) updateUser(newUser)
|
||||
const { error } = await updateUserInDb(user.id, { ...user, ...newUser })
|
||||
const { error } = await updateUserQuery(user.id, { ...user, ...newUser })
|
||||
if (error) showToast({ title: error.name, description: error.message })
|
||||
await refreshUser()
|
||||
setIsSaving(false)
|
@ -1,5 +1,5 @@
|
||||
import { getTestAsset } from '@/test/utils/playwright'
|
||||
import test, { expect } from '@playwright/test'
|
||||
import path from 'path'
|
||||
import { userId } from 'utils/playwright/databaseSetup'
|
||||
|
||||
test.describe.configure({ mode: 'parallel' })
|
||||
@ -14,10 +14,7 @@ test('should display user info properly', async ({ page }) => {
|
||||
).toBeDefined()
|
||||
await page.fill('#name', 'John Doe')
|
||||
expect(saveButton).toBeVisible()
|
||||
await page.setInputFiles(
|
||||
'input[type="file"]',
|
||||
path.join(__dirname, '../fixtures/avatar.jpg')
|
||||
)
|
||||
await page.setInputFiles('input[type="file"]', getTestAsset('avatar.jpg'))
|
||||
await expect(page.locator('img >> nth=1')).toHaveAttribute(
|
||||
'src',
|
||||
new RegExp(
|
@ -15,18 +15,16 @@ import {
|
||||
Flex,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react'
|
||||
import { ConfirmModal } from 'components/modals/ConfirmModal'
|
||||
import { useToast } from 'components/shared/hooks/useToast'
|
||||
import { ConfirmModal } from '@/components/ConfirmModal'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { User } from 'db'
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
ApiTokenFromServer,
|
||||
deleteApiToken,
|
||||
useApiTokens,
|
||||
} from 'services/user/apiTokens'
|
||||
import { timeSince } from 'services/utils'
|
||||
import { byId, isDefined } from 'utils'
|
||||
import { CreateTokenModal } from './CreateTokenModal'
|
||||
import { useApiTokens } from '../../../hooks/useApiTokens'
|
||||
import { ApiTokenFromServer } from '../../../types'
|
||||
import { timeSince } from '@/utils/helpers'
|
||||
import { deleteApiTokenQuery } from '../../../queries/deleteApiTokenQuery'
|
||||
|
||||
type Props = { user: User }
|
||||
|
||||
@ -51,7 +49,7 @@ export const ApiTokensList = ({ user }: Props) => {
|
||||
|
||||
const deleteToken = async (tokenId?: string) => {
|
||||
if (!apiTokens || !tokenId) return
|
||||
const { error } = await deleteApiToken({ userId: user.id, tokenId })
|
||||
const { error } = await deleteApiTokenQuery({ userId: user.id, tokenId })
|
||||
if (!error) mutate({ apiTokens: apiTokens.filter((t) => t.id !== tokenId) })
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { CopyButton } from '@/components/CopyButton'
|
||||
import { createApiTokenQuery } from '../../../queries/createApiTokenQuery'
|
||||
import { ApiTokenFromServer } from '../../../types'
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
@ -13,9 +16,7 @@ import {
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
} from '@chakra-ui/react'
|
||||
import { CopyButton } from 'components/shared/buttons/CopyButton'
|
||||
import React, { FormEvent, useState } from 'react'
|
||||
import { ApiTokenFromServer, createApiToken } from 'services/user/apiTokens'
|
||||
|
||||
type Props = {
|
||||
userId: string
|
||||
@ -37,7 +38,7 @@ export const CreateTokenModal = ({
|
||||
const createToken = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
const { data } = await createApiToken(userId, { name })
|
||||
const { data } = await createApiTokenQuery(userId, { name })
|
||||
if (data?.apiToken) {
|
||||
setNewTokenValue(data.apiToken.token)
|
||||
onNewToken(data.apiToken)
|
@ -10,12 +10,12 @@ import {
|
||||
Flex,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { UploadIcon } from 'assets/icons'
|
||||
import { UploadButton } from 'components/shared/buttons/UploadButton'
|
||||
import { useUser } from 'contexts/UserContext'
|
||||
import { UploadIcon } from '@/components/icons'
|
||||
import React, { ChangeEvent, useState } from 'react'
|
||||
import { isDefined } from 'utils'
|
||||
import { ApiTokensList } from './ApiTokensList'
|
||||
import { UploadButton } from '@/components/ImageUploadContent/UploadButton'
|
||||
import { useUser } from '@/features/account'
|
||||
|
||||
export const MyAccountForm = () => {
|
||||
const {
|
30
apps/builder/src/features/account/hooks/useApiTokens.ts
Normal file
30
apps/builder/src/features/account/hooks/useApiTokens.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { fetcher } from '@/utils/helpers'
|
||||
import useSWR from 'swr'
|
||||
import { env } from 'utils'
|
||||
import { ApiTokenFromServer } from '../types'
|
||||
|
||||
type ServerResponse = {
|
||||
apiTokens: ApiTokenFromServer[]
|
||||
}
|
||||
|
||||
export const useApiTokens = ({
|
||||
userId,
|
||||
onError,
|
||||
}: {
|
||||
userId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<ServerResponse, Error>(
|
||||
userId ? `/api/users/${userId}/api-tokens` : null,
|
||||
fetcher,
|
||||
{
|
||||
dedupingInterval: env('E2E_TEST') === 'true' ? 0 : undefined,
|
||||
}
|
||||
)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
apiTokens: data?.apiTokens,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
3
apps/builder/src/features/account/index.ts
Normal file
3
apps/builder/src/features/account/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { UserProvider, useUser } from './UserProvider'
|
||||
export type { ApiTokenFromServer } from './types'
|
||||
export { MyAccountForm } from './components/MyAccountForm'
|
@ -0,0 +1,14 @@
|
||||
import { sendRequest } from 'utils'
|
||||
import { ApiTokenFromServer } from '../types'
|
||||
|
||||
export const createApiTokenQuery = (
|
||||
userId: string,
|
||||
{ name }: { name: string }
|
||||
) =>
|
||||
sendRequest<{ apiToken: ApiTokenFromServer & { token: string } }>({
|
||||
url: `/api/users/${userId}/api-tokens`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
name,
|
||||
},
|
||||
})
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user