🐛 (usage) Archive typebot to be able to compute usage
This commit is contained in:
committed by
Baptiste Arnaud
parent
75ca255af2
commit
15dbc9577d
@ -26,6 +26,7 @@ import { useWorkspace } from 'contexts/WorkspaceContext'
|
||||
import { EmojiOrImageIcon } from 'components/shared/EmojiOrImageIcon'
|
||||
import { WorkspaceSettingsModal } from './WorkspaceSettingsModal'
|
||||
import { isNotDefined } from 'utils'
|
||||
import { PlanTag } from 'components/shared/PlanTag'
|
||||
|
||||
export const DashboardHeader = () => {
|
||||
const { user } = useUser()
|
||||
@ -90,9 +91,13 @@ export const DashboardHeader = () => {
|
||||
/>
|
||||
</SkeletonCircle>
|
||||
{workspace && (
|
||||
<>
|
||||
<Text noOfLines={1} maxW="200px">
|
||||
{workspace.name}
|
||||
</Text>
|
||||
<PlanTag plan={workspace.plan}/>
|
||||
</>
|
||||
|
||||
)}
|
||||
<ChevronLeftIcon transform="rotate(-90deg)" />
|
||||
</HStack>
|
||||
@ -112,6 +117,7 @@ export const DashboardHeader = () => {
|
||||
defaultIcon={HardDriveIcon}
|
||||
/>
|
||||
<Text>{workspace.name}</Text>
|
||||
<PlanTag plan={workspace.plan}/>
|
||||
</HStack>
|
||||
</MenuItem>
|
||||
))}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Button, HStack, useDisclosure, Text } from '@chakra-ui/react'
|
||||
import { FolderPlusIcon } from 'assets/icons'
|
||||
import { LockTag } from 'components/shared/LockTag'
|
||||
import {
|
||||
LimitReached,
|
||||
ChangePlanModal,
|
||||
} from 'components/shared/modals/ChangePlanModal'
|
||||
import { PlanTag } from 'components/shared/PlanTag'
|
||||
import { useWorkspace } from 'contexts/WorkspaceContext'
|
||||
import { Plan } from 'db'
|
||||
import React from 'react'
|
||||
@ -28,7 +28,7 @@ export const CreateFolderButton = ({ isLoading, onClick }: Props) => {
|
||||
>
|
||||
<HStack>
|
||||
<Text>Create a folder</Text>
|
||||
{isFreePlan(workspace) && <PlanTag plan={Plan.STARTER} />}
|
||||
{isFreePlan(workspace) && <LockTag plan={Plan.STARTER} />}
|
||||
</HStack>
|
||||
<ChangePlanModal
|
||||
isOpen={isOpen}
|
||||
|
@ -46,7 +46,7 @@ export const CurrentSubscriptionContent = ({
|
||||
return (
|
||||
<Stack gap="2">
|
||||
<Heading fontSize="3xl">Subscription</Heading>
|
||||
<HStack>
|
||||
<HStack data-testid="current-subscription">
|
||||
<Text>Current workspace subscription: </Text>
|
||||
{isCancelling ? (
|
||||
<Spinner color="gray.500" size="xs" />
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { HStack, Text, Tooltip } from '@chakra-ui/react'
|
||||
import { PlanTag } from 'components/shared/PlanTag'
|
||||
import { LockTag } from 'components/shared/LockTag'
|
||||
import { useWorkspace } from 'contexts/WorkspaceContext'
|
||||
import { Plan } from 'db'
|
||||
import {
|
||||
@ -54,7 +54,7 @@ export const BlockTypeLabel = ({ type }: Props): JSX.Element => {
|
||||
<Tooltip label="Upload Files">
|
||||
<HStack>
|
||||
<Text>File</Text>
|
||||
{isFreePlan(workspace) && <PlanTag plan={Plan.STARTER} />}
|
||||
{isFreePlan(workspace) && <LockTag plan={Plan.STARTER} />}
|
||||
</HStack>
|
||||
</Tooltip>
|
||||
)
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Flex, FormLabel, Stack, Switch, useDisclosure } from '@chakra-ui/react'
|
||||
import { LockTag } from 'components/shared/LockTag'
|
||||
import {
|
||||
ChangePlanModal,
|
||||
LimitReached,
|
||||
} from 'components/shared/modals/ChangePlanModal'
|
||||
import { PlanTag } from 'components/shared/PlanTag'
|
||||
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||
import { useWorkspace } from 'contexts/WorkspaceContext'
|
||||
import { Plan } from 'db'
|
||||
@ -66,7 +66,7 @@ export const GeneralSettingsForm = ({
|
||||
>
|
||||
<FormLabel htmlFor="branding" mb="0">
|
||||
Typebot.io branding{' '}
|
||||
{isWorkspaceFreePlan && <PlanTag plan={Plan.STARTER} />}
|
||||
{isWorkspaceFreePlan && <LockTag plan={Plan.STARTER} />}
|
||||
</FormLabel>
|
||||
<Switch
|
||||
id="branding"
|
||||
|
@ -10,8 +10,8 @@ import {
|
||||
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 { PlanTag } from 'components/shared/PlanTag'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import { useWorkspace } from 'contexts/WorkspaceContext'
|
||||
import { Plan } from 'db'
|
||||
@ -82,19 +82,22 @@ export const ShareContent = () => {
|
||||
/>
|
||||
</HStack>
|
||||
)}
|
||||
{isWorkspaceProPlan(workspace) &&
|
||||
isNotDefined(typebot?.customDomain) ? (
|
||||
<CustomDomainsDropdown
|
||||
onCustomDomainSelect={handleCustomDomainChange}
|
||||
/>
|
||||
) : (
|
||||
<UpgradeButton
|
||||
colorScheme="gray"
|
||||
limitReachedType={LimitReached.CUSTOM_DOMAIN}
|
||||
>
|
||||
<Text mr="2">Add my domain</Text> <PlanTag plan={Plan.PRO} />
|
||||
</UpgradeButton>
|
||||
)}
|
||||
{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}>
|
||||
|
14
apps/builder/components/shared/LockTag.tsx
Normal file
14
apps/builder/components/shared/LockTag.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { Tag, TagProps } from '@chakra-ui/react'
|
||||
import { LockedIcon } from 'assets/icons'
|
||||
import { Plan } from 'db'
|
||||
import { planColorSchemes } from './PlanTag'
|
||||
|
||||
export const LockTag = ({ plan, ...props }: { plan?: Plan } & TagProps) => (
|
||||
<Tag
|
||||
colorScheme={plan ? planColorSchemes[plan] : 'gray'}
|
||||
data-testid={`${plan?.toLowerCase()}-lock-tag`}
|
||||
{...props}
|
||||
>
|
||||
<LockedIcon />
|
||||
</Tag>
|
||||
)
|
@ -1,18 +1,34 @@
|
||||
import { Tag, TagProps } from '@chakra-ui/react'
|
||||
import { Tag, TagProps, ThemeTypings } from '@chakra-ui/react'
|
||||
import { Plan } from 'db'
|
||||
|
||||
export const planColorSchemes: Record<Plan, ThemeTypings['colorSchemes']> = {
|
||||
[Plan.LIFETIME]: 'purple',
|
||||
[Plan.PRO]: 'blue',
|
||||
[Plan.OFFERED]: 'orange',
|
||||
[Plan.STARTER]: 'orange',
|
||||
[Plan.FREE]: 'gray',
|
||||
}
|
||||
|
||||
export const PlanTag = ({ plan, ...props }: { plan?: Plan } & TagProps) => {
|
||||
switch (plan) {
|
||||
case Plan.LIFETIME: {
|
||||
return (
|
||||
<Tag colorScheme="purple" data-testid="lifetime-plan-tag" {...props}>
|
||||
<Tag
|
||||
colorScheme={planColorSchemes[plan]}
|
||||
data-testid="lifetime-plan-tag"
|
||||
{...props}
|
||||
>
|
||||
Lifetime
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
case Plan.PRO: {
|
||||
return (
|
||||
<Tag colorScheme="blue" data-testid="pro-plan-tag" {...props}>
|
||||
<Tag
|
||||
colorScheme={planColorSchemes[plan]}
|
||||
data-testid="pro-plan-tag"
|
||||
{...props}
|
||||
>
|
||||
Pro
|
||||
</Tag>
|
||||
)
|
||||
@ -20,14 +36,22 @@ export const PlanTag = ({ plan, ...props }: { plan?: Plan } & TagProps) => {
|
||||
case Plan.OFFERED:
|
||||
case Plan.STARTER: {
|
||||
return (
|
||||
<Tag colorScheme="orange" data-testid="starter-plan-tag" {...props}>
|
||||
<Tag
|
||||
colorScheme={planColorSchemes[plan]}
|
||||
data-testid="starter-plan-tag"
|
||||
{...props}
|
||||
>
|
||||
Starter
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<Tag colorScheme="gray" data-testid="free-plan-tag" {...props}>
|
||||
<Tag
|
||||
colorScheme={planColorSchemes[Plan.FREE]}
|
||||
data-testid="free-plan-tag"
|
||||
{...props}
|
||||
>
|
||||
Free
|
||||
</Tag>
|
||||
)
|
||||
|
@ -139,13 +139,14 @@ export const WorkspaceContext = ({ children }: WorkspaceContextProps) => {
|
||||
if (!currentWorkspace || !workspaces || workspaces.length < 2) return
|
||||
const { data } = await deleteWorkspace(currentWorkspace.id)
|
||||
if (!data || !currentWorkspace) return
|
||||
setCurrentWorkspace(workspaces[0])
|
||||
const newWorkspaces = (workspaces ?? []).filter((w) =>
|
||||
w.id === currentWorkspace.id
|
||||
? { ...data.workspace, members: w.members }
|
||||
: w
|
||||
)
|
||||
setCurrentWorkspace(newWorkspaces[0])
|
||||
mutate({
|
||||
workspaces: (workspaces ?? []).filter((w) =>
|
||||
w.id === currentWorkspace.id
|
||||
? { ...data.workspace, members: w.members }
|
||||
: w
|
||||
),
|
||||
workspaces: newWorkspaces,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
{
|
||||
workspace: { members: { some: { userId: user.id } } },
|
||||
id: { in: typebotIds },
|
||||
isArchived: { not: true },
|
||||
},
|
||||
{
|
||||
id: { in: typebotIds },
|
||||
@ -39,6 +40,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
isArchived: { not: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -51,6 +53,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
isArchived: { not: true },
|
||||
folderId,
|
||||
workspace: {
|
||||
id: workspaceId,
|
||||
@ -63,6 +66,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
},
|
||||
},
|
||||
{
|
||||
isArchived: { not: true },
|
||||
workspace: {
|
||||
id: workspaceId,
|
||||
members: {
|
||||
|
@ -13,13 +13,17 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const typebotId = req.query.typebotId as string
|
||||
if (req.method === 'GET') {
|
||||
const typebot = await prisma.typebot.findFirst({
|
||||
where: canReadTypebot(typebotId, user),
|
||||
where: {
|
||||
...canReadTypebot(typebotId, user),
|
||||
isArchived: { not: true },
|
||||
},
|
||||
include: {
|
||||
publishedTypebot: true,
|
||||
collaborators: { select: { userId: true, type: true } },
|
||||
webhooks: true,
|
||||
},
|
||||
})
|
||||
console.log(typebot)
|
||||
if (!typebot) return res.send({ typebot: null })
|
||||
const { publishedTypebot, collaborators, webhooks, ...restOfTypebot } =
|
||||
typebot
|
||||
@ -35,8 +39,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
}
|
||||
|
||||
if (req.method === 'DELETE') {
|
||||
const typebots = await prisma.typebot.deleteMany({
|
||||
const typebots = await prisma.typebot.updateMany({
|
||||
where: canWriteTypebot(typebotId, user),
|
||||
data: { isArchived: true },
|
||||
})
|
||||
await archiveResults(res)({
|
||||
typebotId,
|
||||
|
@ -2,7 +2,7 @@ import { withSentry } from '@sentry/nextjs'
|
||||
import { Prisma, Workspace, WorkspaceRole } from 'db'
|
||||
import prisma from 'libs/prisma'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { archiveResults, getAuthenticatedUser } from 'services/api/utils'
|
||||
import { getAuthenticatedUser } from 'services/api/utils'
|
||||
import { methodNotAllowed, notAuthenticated } from 'utils/api'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
@ -28,23 +28,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
id,
|
||||
members: { some: { userId: user.id, role: WorkspaceRole.ADMIN } },
|
||||
}
|
||||
const deletedTypebots = await prisma.typebot.findMany({
|
||||
where: {
|
||||
workspace: workspaceFilter,
|
||||
},
|
||||
})
|
||||
await prisma.workspace.deleteMany({
|
||||
where: workspaceFilter,
|
||||
})
|
||||
await Promise.all(
|
||||
deletedTypebots.map((typebot) =>
|
||||
archiveResults(res)({
|
||||
typebotId: typebot.id,
|
||||
user,
|
||||
resultsFilter: { typebotId: typebot.id },
|
||||
})
|
||||
)
|
||||
)
|
||||
return res.status(200).json({
|
||||
message: 'success',
|
||||
})
|
||||
|
@ -102,14 +102,16 @@ export const setupDatabase = async () => {
|
||||
return setupCredentials()
|
||||
}
|
||||
|
||||
export const setupWorkspaces = async () =>
|
||||
prisma.workspace.createMany({
|
||||
export const setupWorkspaces = async () => {
|
||||
await prisma.workspace.create({
|
||||
data: {
|
||||
id: freeWorkspaceId,
|
||||
name: 'Free workspace',
|
||||
plan: Plan.FREE,
|
||||
},
|
||||
})
|
||||
await prisma.workspace.createMany({
|
||||
data: [
|
||||
{
|
||||
id: freeWorkspaceId,
|
||||
name: 'Free workspace',
|
||||
plan: Plan.FREE,
|
||||
},
|
||||
{
|
||||
id: starterWorkspaceId,
|
||||
name: 'Starter workspace',
|
||||
@ -128,6 +130,7 @@ export const setupWorkspaces = async () =>
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
export const createWorkspaces = async (workspaces: Partial<Workspace>[]) => {
|
||||
const workspaceIds = workspaces.map((workspace) => workspace.id ?? cuid())
|
||||
@ -231,11 +234,15 @@ export const getSignedInUser = (email: string) =>
|
||||
prisma.user.findFirst({ where: { email } })
|
||||
|
||||
export const createTypebots = async (partialTypebots: Partial<Typebot>[]) => {
|
||||
const typebotsWithId = partialTypebots.map((typebot) => ({
|
||||
...typebot,
|
||||
id: typebot.id ?? cuid(),
|
||||
}))
|
||||
await prisma.typebot.createMany({
|
||||
data: partialTypebots.map(parseTestTypebot),
|
||||
data: typebotsWithId.map(parseTestTypebot),
|
||||
})
|
||||
return prisma.publicTypebot.createMany({
|
||||
data: partialTypebots.map((t) =>
|
||||
data: typebotsWithId.map((t) =>
|
||||
parseTypebotToPublicTypebot(t.id + '-public', parseTestTypebot(t))
|
||||
),
|
||||
})
|
||||
@ -304,7 +311,7 @@ const parseTypebotToPublicTypebot = (
|
||||
})
|
||||
|
||||
const parseTestTypebot = (partialTypebot: Partial<Typebot>): Typebot => ({
|
||||
id: partialTypebot.id ?? 'typebot',
|
||||
id: cuid(),
|
||||
workspaceId: proWorkspaceId,
|
||||
folderId: null,
|
||||
name: 'My typebot',
|
||||
|
@ -165,9 +165,13 @@ test('plan changes should work', async ({ page }) => {
|
||||
await page.goto('/typebots')
|
||||
await page.click('text=Settings & Members')
|
||||
await page.click('text=Billing & Usage')
|
||||
await expect(page.locator('[data-testid="pro-plan-tag"]')).toBeVisible()
|
||||
await expect(page.locator('[data-testid="current-subscription"]')).toHaveText(
|
||||
'Current workspace subscription: ProCancel my subscription'
|
||||
)
|
||||
await page.click('button >> text="Cancel my subscription"')
|
||||
await expect(page.locator('[data-testid="free-plan-tag"]')).toBeVisible()
|
||||
await expect(page.locator('[data-testid="current-subscription"]')).toHaveText(
|
||||
'Current workspace subscription: Free'
|
||||
)
|
||||
})
|
||||
|
||||
test('should display invoices', async ({ page }) => {
|
||||
|
@ -57,7 +57,7 @@ test.describe('Starter workspace', () => {
|
||||
},
|
||||
])
|
||||
await page.goto(`/typebots/${typebotId}/share`)
|
||||
await expect(page.locator('text=Pro')).toBeVisible()
|
||||
await expect(page.locator('[data-testid="pro-lock-tag"]')).toBeVisible()
|
||||
await page.click('text=Add my domain')
|
||||
await expect(
|
||||
page.locator(
|
||||
|
@ -75,7 +75,7 @@ test.describe('Free user', () => {
|
||||
await page.goto('/typebots')
|
||||
await page.click('text="Pro workspace"')
|
||||
await page.click('text="Free workspace"')
|
||||
await expect(page.locator('[data-testid="starter-plan-tag"]')).toBeVisible()
|
||||
await expect(page.locator('[data-testid="starter-lock-tag"]')).toBeVisible()
|
||||
await page.click('text=Create a folder')
|
||||
await expect(
|
||||
page.locator(
|
||||
|
@ -135,7 +135,9 @@ test.describe.parallel('Settings page', () => {
|
||||
typebotViewer(page).locator('text="What\'s your name?"')
|
||||
).toBeVisible()
|
||||
await page.click('button:has-text("General")')
|
||||
await expect(page.locator('text=Starter')).toBeVisible()
|
||||
await expect(
|
||||
page.locator('[data-testid="starter-lock-tag"]')
|
||||
).toBeVisible()
|
||||
await page.click('text=Typebot.io branding')
|
||||
await expect(
|
||||
page.locator(
|
||||
|
@ -144,7 +144,7 @@ test('can manage members', async ({ page }) => {
|
||||
|
||||
test("can't add new members when limit is reached", async ({ page }) => {
|
||||
await page.goto('/typebots')
|
||||
await page.click('text="Pro workspace"')
|
||||
await page.click('text="My awesome workspace"')
|
||||
await page.click('text="Free workspace"')
|
||||
await page.click('text=Settings & Members')
|
||||
await page.click('text="Members"')
|
||||
|
Reference in New Issue
Block a user