♻️ (builder) Change to features-centric folder structure
This commit is contained in:
committed by
Baptiste Arnaud
parent
3686465a85
commit
643571fe7d
2
.gitignore
vendored
2
.gitignore
vendored
@ -9,6 +9,8 @@ authenticatedState.json
|
|||||||
playwright-report
|
playwright-report
|
||||||
dist
|
dist
|
||||||
test-results
|
test-results
|
||||||
|
test/results
|
||||||
|
test/report
|
||||||
**/api/scripts
|
**/api/scripts
|
||||||
.sentryclirc
|
.sentryclirc
|
||||||
.docusaurus
|
.docusaurus
|
||||||
|
@ -26,5 +26,18 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
'react/no-unescaped-entities': [0],
|
'react/no-unescaped-entities': [0],
|
||||||
'react/display-name': [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",
|
"immer": "9.0.16",
|
||||||
"js-video-url-parser": "0.5.1",
|
"js-video-url-parser": "0.5.1",
|
||||||
"jsonwebtoken": "8.5.1",
|
"jsonwebtoken": "8.5.1",
|
||||||
"kbar": "0.1.0-beta.36",
|
|
||||||
"micro": "9.4.1",
|
"micro": "9.4.1",
|
||||||
"micro-cors": "0.1.1",
|
"micro-cors": "0.1.1",
|
||||||
"minio": "7.0.32",
|
"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: {
|
use: {
|
||||||
...playwrightBaseConfig.use,
|
...playwrightBaseConfig.use,
|
||||||
baseURL: process.env.NEXTAUTH_URL,
|
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
|
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 { useEffect, useRef, useState } from 'react'
|
||||||
import { useDebouncedCallback } from 'use-debounce'
|
import { useDebouncedCallback } from 'use-debounce'
|
||||||
import { linter, LintSource } from '@codemirror/lint'
|
import { linter, LintSource } from '@codemirror/lint'
|
||||||
import { VariablesButton } from './buttons/VariablesButton'
|
import { VariablesButton } from '@/features/variables'
|
||||||
import { Variable } from 'models'
|
import { Variable } from 'models'
|
||||||
import { env } from 'utils'
|
import { env } from 'utils'
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
|||||||
Portal,
|
Portal,
|
||||||
Stack,
|
Stack,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { ChevronLeftIcon } from 'assets/icons'
|
import { ChevronLeftIcon } from '@/components/icons'
|
||||||
import React, { ReactNode } from 'react'
|
import React, { ReactNode } from 'react'
|
||||||
|
|
||||||
type Props<T> = {
|
type Props<T> = {
|
@ -1,4 +1,4 @@
|
|||||||
import { ToolIcon } from 'assets/icons'
|
import { ToolIcon } from '@/components/icons'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { chakra, IconProps, Image } from '@chakra-ui/react'
|
import { chakra, IconProps, Image } from '@chakra-ui/react'
|
||||||
|
|
@ -1,10 +1,10 @@
|
|||||||
import { Flex, Stack, Text } from '@chakra-ui/react'
|
import { Flex, Stack, Text } from '@chakra-ui/react'
|
||||||
import { GiphyFetch } from '@giphy/js-fetch-api'
|
import { GiphyFetch } from '@giphy/js-fetch-api'
|
||||||
import { Grid } from '@giphy/react-components'
|
import { Grid } from '@giphy/react-components'
|
||||||
import { GiphyLogo } from 'assets/logos'
|
import { GiphyLogo } from './GiphyLogo'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { env, isEmpty } from 'utils'
|
import { env, isEmpty } from 'utils'
|
||||||
import { Input } from '../Textbox'
|
import { Input } from '../inputs'
|
||||||
|
|
||||||
type GiphySearchFormProps = {
|
type GiphySearchFormProps = {
|
||||||
onSubmit: (url: string) => void
|
onSubmit: (url: string) => void
|
@ -1,8 +1,8 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Button, Flex, HStack, Stack } from '@chakra-ui/react'
|
import { Button, Flex, HStack, Stack } from '@chakra-ui/react'
|
||||||
import { UploadButton } from '../buttons/UploadButton'
|
import { UploadButton } from './UploadButton'
|
||||||
import { GiphySearchForm } from './GiphySearchForm'
|
import { GiphySearchForm } from './GiphySearchForm'
|
||||||
import { Input } from '../Textbox/Input'
|
import { Input } from '../inputs/Input'
|
||||||
import { EmojiSearchableList } from './emoji/EmojiSearchableList'
|
import { EmojiSearchableList } from './emoji/EmojiSearchableList'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
@ -1,6 +1,6 @@
|
|||||||
|
import { compressFile } from '@/utils/helpers'
|
||||||
import { Button, ButtonProps, chakra } from '@chakra-ui/react'
|
import { Button, ButtonProps, chakra } from '@chakra-ui/react'
|
||||||
import React, { ChangeEvent, useState } from 'react'
|
import { ChangeEvent, useState } from 'react'
|
||||||
import { compressFile } from 'services/utils'
|
|
||||||
import { uploadFiles } from 'utils'
|
import { uploadFiles } from 'utils'
|
||||||
|
|
||||||
type UploadButtonProps = {
|
type UploadButtonProps = {
|
@ -1,6 +1,5 @@
|
|||||||
import { Tooltip, chakra } from '@chakra-ui/react'
|
import { Tooltip, chakra } from '@chakra-ui/react'
|
||||||
import { HelpCircleIcon } from 'assets/icons'
|
import { HelpCircleIcon } from './icons'
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
@ -14,7 +14,7 @@ import { Variable } from 'models'
|
|||||||
import { useState, useRef, useEffect, ChangeEvent } from 'react'
|
import { useState, useRef, useEffect, ChangeEvent } from 'react'
|
||||||
import { useDebouncedCallback } from 'use-debounce'
|
import { useDebouncedCallback } from 'use-debounce'
|
||||||
import { env, isDefined } from 'utils'
|
import { env, isDefined } from 'utils'
|
||||||
import { VariablesButton } from './buttons/VariablesButton'
|
import { VariablesButton } from '@/features/variables'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectedItem?: string
|
selectedItem?: string
|
@ -1,10 +1,10 @@
|
|||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from '@/features/editor'
|
||||||
import { useUser } from 'contexts/UserContext'
|
import { useUser } from '@/features/account'
|
||||||
import { useWorkspace } from 'contexts/WorkspaceContext'
|
import { useWorkspace } from '@/features/workspace'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { isCloudProdInstance } from 'services/utils'
|
|
||||||
import { planToReadable } from 'services/workspace'
|
|
||||||
import { initBubble } from 'typebot-js'
|
import { initBubble } from 'typebot-js'
|
||||||
|
import { isCloudProdInstance } from '@/utils/helpers'
|
||||||
|
import { planToReadable } from '@/features/billing'
|
||||||
|
|
||||||
export const SupportBubble = () => {
|
export const SupportBubble = () => {
|
||||||
const { typebot } = useTypebot()
|
const { typebot } = useTypebot()
|
@ -1,5 +1,5 @@
|
|||||||
import { Box, Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react'
|
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 cuid from 'cuid'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
import Link, { LinkProps } from 'next/link'
|
import Link, { LinkProps } from 'next/link'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { chakra, HStack, TextProps } from '@chakra-ui/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 }
|
type TextLinkProps = LinkProps & TextProps & { isExternal?: boolean }
|
||||||
|
|
@ -8,21 +8,9 @@ import {
|
|||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { ChangePlanModal } from './modals/ChangePlanModal'
|
import { ChangePlanModal, LimitReached } from '@/features/billing'
|
||||||
import { LimitReached } from './modals/ChangePlanModal'
|
|
||||||
|
|
||||||
export const Info = (props: AlertProps) => (
|
export const UnlockPlanAlertInfo = ({
|
||||||
<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 = ({
|
|
||||||
contentLabel,
|
contentLabel,
|
||||||
buttonLabel = 'More info',
|
buttonLabel = 'More info',
|
||||||
type,
|
type,
|
@ -11,8 +11,8 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
HStack,
|
HStack,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { EditIcon, PlusIcon, TrashIcon } from 'assets/icons'
|
import { EditIcon, PlusIcon, TrashIcon } from '@/components/icons'
|
||||||
import { useTypebot } from 'contexts/TypebotContext'
|
import { useTypebot } from '@/features/editor'
|
||||||
import cuid from 'cuid'
|
import cuid from 'cuid'
|
||||||
import { Variable } from 'models'
|
import { Variable } from 'models'
|
||||||
import React, { useState, useRef, ChangeEvent, useEffect } from 'react'
|
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 React, { ChangeEvent, useEffect, useRef, useState } from 'react'
|
||||||
import { useDebouncedCallback } from 'use-debounce'
|
import { useDebouncedCallback } from 'use-debounce'
|
||||||
import { env } from 'utils'
|
import { env } from 'utils'
|
||||||
import { VariablesButton } from '../buttons/VariablesButton'
|
import { VariablesButton } from '../../features/variables/components/VariablesButton'
|
||||||
import { MoreInfoTooltip } from '../MoreInfoTooltip'
|
import { MoreInfoTooltip } from '../MoreInfoTooltip'
|
||||||
|
|
||||||
export type TextBoxProps = {
|
export type TextBoxProps = {
|
@ -1,2 +1,3 @@
|
|||||||
export { Input } from './Input'
|
export { Input } from './Input'
|
||||||
export { Textarea } from './Textarea'
|
export { Textarea } from './Textarea'
|
||||||
|
export { SmartNumberInput } from './SmartNumberInput'
|
@ -9,11 +9,11 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { isDefined, isNotDefined } from 'utils'
|
import { isDefined, isNotDefined } from 'utils'
|
||||||
import { updateUser as updateUserInDb } from 'services/user/user'
|
|
||||||
import { dequal } from 'dequal'
|
import { dequal } from 'dequal'
|
||||||
import { User } from 'db'
|
import { User } from 'db'
|
||||||
import { setUser as setSentryUser } from '@sentry/nextjs'
|
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<{
|
const userContext = createContext<{
|
||||||
user?: User
|
user?: User
|
||||||
@ -28,7 +28,7 @@ const userContext = createContext<{
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
export const UserContext = ({ children }: { children: ReactNode }) => {
|
export const UserProvider = ({ children }: { children: ReactNode }) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { data: session, status } = useSession()
|
const { data: session, status } = useSession()
|
||||||
const [user, setUser] = useState<User | undefined>()
|
const [user, setUser] = useState<User | undefined>()
|
||||||
@ -84,7 +84,7 @@ export const UserContext = ({ children }: { children: ReactNode }) => {
|
|||||||
if (isNotDefined(user)) return
|
if (isNotDefined(user)) return
|
||||||
setIsSaving(true)
|
setIsSaving(true)
|
||||||
if (newUser) updateUser(newUser)
|
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 })
|
if (error) showToast({ title: error.name, description: error.message })
|
||||||
await refreshUser()
|
await refreshUser()
|
||||||
setIsSaving(false)
|
setIsSaving(false)
|
@ -1,5 +1,5 @@
|
|||||||
|
import { getTestAsset } from '@/test/utils/playwright'
|
||||||
import test, { expect } from '@playwright/test'
|
import test, { expect } from '@playwright/test'
|
||||||
import path from 'path'
|
|
||||||
import { userId } from 'utils/playwright/databaseSetup'
|
import { userId } from 'utils/playwright/databaseSetup'
|
||||||
|
|
||||||
test.describe.configure({ mode: 'parallel' })
|
test.describe.configure({ mode: 'parallel' })
|
||||||
@ -14,10 +14,7 @@ test('should display user info properly', async ({ page }) => {
|
|||||||
).toBeDefined()
|
).toBeDefined()
|
||||||
await page.fill('#name', 'John Doe')
|
await page.fill('#name', 'John Doe')
|
||||||
expect(saveButton).toBeVisible()
|
expect(saveButton).toBeVisible()
|
||||||
await page.setInputFiles(
|
await page.setInputFiles('input[type="file"]', getTestAsset('avatar.jpg'))
|
||||||
'input[type="file"]',
|
|
||||||
path.join(__dirname, '../fixtures/avatar.jpg')
|
|
||||||
)
|
|
||||||
await expect(page.locator('img >> nth=1')).toHaveAttribute(
|
await expect(page.locator('img >> nth=1')).toHaveAttribute(
|
||||||
'src',
|
'src',
|
||||||
new RegExp(
|
new RegExp(
|
@ -15,18 +15,16 @@ import {
|
|||||||
Flex,
|
Flex,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { ConfirmModal } from 'components/modals/ConfirmModal'
|
import { ConfirmModal } from '@/components/ConfirmModal'
|
||||||
import { useToast } from 'components/shared/hooks/useToast'
|
import { useToast } from '@/hooks/useToast'
|
||||||
import { User } from 'db'
|
import { User } from 'db'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import {
|
|
||||||
ApiTokenFromServer,
|
|
||||||
deleteApiToken,
|
|
||||||
useApiTokens,
|
|
||||||
} from 'services/user/apiTokens'
|
|
||||||
import { timeSince } from 'services/utils'
|
|
||||||
import { byId, isDefined } from 'utils'
|
import { byId, isDefined } from 'utils'
|
||||||
import { CreateTokenModal } from './CreateTokenModal'
|
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 }
|
type Props = { user: User }
|
||||||
|
|
||||||
@ -51,7 +49,7 @@ export const ApiTokensList = ({ user }: Props) => {
|
|||||||
|
|
||||||
const deleteToken = async (tokenId?: string) => {
|
const deleteToken = async (tokenId?: string) => {
|
||||||
if (!apiTokens || !tokenId) return
|
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) })
|
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 {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
@ -13,9 +16,7 @@ import {
|
|||||||
InputGroup,
|
InputGroup,
|
||||||
InputRightElement,
|
InputRightElement,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { CopyButton } from 'components/shared/buttons/CopyButton'
|
|
||||||
import React, { FormEvent, useState } from 'react'
|
import React, { FormEvent, useState } from 'react'
|
||||||
import { ApiTokenFromServer, createApiToken } from 'services/user/apiTokens'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
userId: string
|
userId: string
|
||||||
@ -37,7 +38,7 @@ export const CreateTokenModal = ({
|
|||||||
const createToken = async (e: FormEvent) => {
|
const createToken = async (e: FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
const { data } = await createApiToken(userId, { name })
|
const { data } = await createApiTokenQuery(userId, { name })
|
||||||
if (data?.apiToken) {
|
if (data?.apiToken) {
|
||||||
setNewTokenValue(data.apiToken.token)
|
setNewTokenValue(data.apiToken.token)
|
||||||
onNewToken(data.apiToken)
|
onNewToken(data.apiToken)
|
@ -10,12 +10,12 @@ import {
|
|||||||
Flex,
|
Flex,
|
||||||
Text,
|
Text,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { UploadIcon } from 'assets/icons'
|
import { UploadIcon } from '@/components/icons'
|
||||||
import { UploadButton } from 'components/shared/buttons/UploadButton'
|
|
||||||
import { useUser } from 'contexts/UserContext'
|
|
||||||
import React, { ChangeEvent, useState } from 'react'
|
import React, { ChangeEvent, useState } from 'react'
|
||||||
import { isDefined } from 'utils'
|
import { isDefined } from 'utils'
|
||||||
import { ApiTokensList } from './ApiTokensList'
|
import { ApiTokensList } from './ApiTokensList'
|
||||||
|
import { UploadButton } from '@/components/ImageUploadContent/UploadButton'
|
||||||
|
import { useUser } from '@/features/account'
|
||||||
|
|
||||||
export const MyAccountForm = () => {
|
export const MyAccountForm = () => {
|
||||||
const {
|
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'
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user