From ec470b578c1d2aa769838b566d60a3c652164fac Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Sat, 12 Feb 2022 12:54:16 +0100 Subject: [PATCH] =?UTF-8?q?feat(results):=20=F0=9F=9B=82=20Limit=20incompl?= =?UTF-8?q?ete=20submissions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/analytics/StatsCards.tsx | 4 +- .../FolderContent/CreateFolderButton.tsx | 13 +++--- apps/builder/components/shared/Info.tsx | 41 ++++++++++++++++++- .../modals/UpgradeModal./UpgradeModal.tsx | 13 ++---- .../layouts/results/ResultsContent.tsx | 8 ++++ .../layouts/results/SubmissionContent.tsx | 20 +++++---- .../pages/api/typebots/[typebotId]/results.ts | 2 + .../api/typebots/[typebotId]/results/stats.ts | 2 +- apps/builder/playwright/tests/results.spec.ts | 12 ++++++ .../src/components/ChatBlock/ChatBlock.tsx | 8 ++-- packages/models/src/answer.ts | 2 +- 11 files changed, 93 insertions(+), 32 deletions(-) diff --git a/apps/builder/components/analytics/StatsCards.tsx b/apps/builder/components/analytics/StatsCards.tsx index feeb5ca3e7..7eb32e611f 100644 --- a/apps/builder/components/analytics/StatsCards.tsx +++ b/apps/builder/components/analytics/StatsCards.tsx @@ -34,7 +34,9 @@ export const StatsCards = ({ Completion rate {stats ? ( - {stats.completionRate}% + + {Math.round((stats.totalCompleted / stats.totalStarts) * 100)}% + ) : ( )} diff --git a/apps/builder/components/dashboard/FolderContent/CreateFolderButton.tsx b/apps/builder/components/dashboard/FolderContent/CreateFolderButton.tsx index 9802d4e9f9..cac849b7c0 100644 --- a/apps/builder/components/dashboard/FolderContent/CreateFolderButton.tsx +++ b/apps/builder/components/dashboard/FolderContent/CreateFolderButton.tsx @@ -26,14 +26,11 @@ export const CreateFolderButton = ({ isLoading, onClick }: Props) => { Create a folder {isFreePlan(user) && Pro} - {user && ( - - )} + ) } diff --git a/apps/builder/components/shared/Info.tsx b/apps/builder/components/shared/Info.tsx index 91b964ecf5..86bdb7a6a2 100644 --- a/apps/builder/components/shared/Info.tsx +++ b/apps/builder/components/shared/Info.tsx @@ -1,5 +1,15 @@ -import { Alert, AlertIcon, AlertProps } from '@chakra-ui/react' +import { + Alert, + AlertIcon, + AlertProps, + Button, + HStack, + Text, + useDisclosure, +} from '@chakra-ui/react' import React from 'react' +import { UpgradeModal } from './modals/UpgradeModal.' +import { LimitReached } from './modals/UpgradeModal./UpgradeModal' export const Info = (props: AlertProps) => ( @@ -11,3 +21,32 @@ export const Info = (props: AlertProps) => ( export const PublishFirstInfo = (props: AlertProps) => ( You need to publish your typebot first ) + +export const UnlockProPlanInfo = ({ + contentLabel, + buttonLabel, + type, +}: { + contentLabel: string + buttonLabel: string + type?: LimitReached +}) => { + const { isOpen, onOpen, onClose } = useDisclosure() + return ( + + + + {contentLabel} + + + + + ) +} diff --git a/apps/builder/components/shared/modals/UpgradeModal./UpgradeModal.tsx b/apps/builder/components/shared/modals/UpgradeModal./UpgradeModal.tsx index 64c7d7f547..c5d8b43e7d 100644 --- a/apps/builder/components/shared/modals/UpgradeModal./UpgradeModal.tsx +++ b/apps/builder/components/shared/modals/UpgradeModal./UpgradeModal.tsx @@ -13,8 +13,8 @@ import { } from '@chakra-ui/react' import { PricingCard } from './PricingCard' import { ActionButton } from './ActionButton' -import { User } from 'db' import { pay } from 'services/stripe' +import { useUser } from 'contexts/UserContext' export enum LimitReached { BRAND = 'Remove branding', @@ -24,18 +24,13 @@ export enum LimitReached { } type UpgradeModalProps = { - user: User - type: LimitReached + type?: LimitReached isOpen: boolean onClose: () => void } -export const UpgradeModal = ({ - type, - user, - onClose, - isOpen, -}: UpgradeModalProps) => { +export const UpgradeModal = ({ type, onClose, isOpen }: UpgradeModalProps) => { + const { user } = useUser() const [payLoading, setPayLoading] = useState(false) const [userLanguage, setUserLanguage] = useState('en') diff --git a/apps/builder/layouts/results/ResultsContent.tsx b/apps/builder/layouts/results/ResultsContent.tsx index 4e6972445c..8c9e26786a 100644 --- a/apps/builder/layouts/results/ResultsContent.tsx +++ b/apps/builder/layouts/results/ResultsContent.tsx @@ -1,14 +1,17 @@ import { Button, Flex, HStack, Tag, useToast, Text } from '@chakra-ui/react' import { NextChakraLink } from 'components/nextChakra/NextChakraLink' import { useTypebot } from 'contexts/TypebotContext/TypebotContext' +import { useUser } from 'contexts/UserContext' import { useRouter } from 'next/router' import React, { useMemo } from 'react' import { useStats } from 'services/analytics' +import { isFreePlan } from 'services/user' import { AnalyticsContent } from './AnalyticsContent' import { SubmissionsContent } from './SubmissionContent' export const ResultsContent = () => { const router = useRouter() + const { user } = useUser() const { typebot } = useTypebot() const isAnalytics = useMemo( () => router.pathname.endsWith('analytics'), @@ -76,6 +79,11 @@ export const ResultsContent = () => { typebotId={typebot.id} onDeleteResults={handleDeletedResults} totalResults={stats?.totalStarts ?? 0} + totalHiddenResults={ + isFreePlan(user) + ? (stats?.totalStarts ?? 0) - (stats?.totalCompleted ?? 0) + : undefined + } /> ))} diff --git a/apps/builder/layouts/results/SubmissionContent.tsx b/apps/builder/layouts/results/SubmissionContent.tsx index 8344f994bd..93be81b563 100644 --- a/apps/builder/layouts/results/SubmissionContent.tsx +++ b/apps/builder/layouts/results/SubmissionContent.tsx @@ -10,15 +10,18 @@ import { useResults, } from 'services/results' import { unparse } from 'papaparse' +import { UnlockProPlanInfo } from 'components/shared/Info' type Props = { typebotId: string totalResults: number + totalHiddenResults?: number onDeleteResults: (total: number) => void } export const SubmissionsContent = ({ typebotId, totalResults, + totalHiddenResults, onDeleteResults, }: Props) => { const [selectedIndices, setSelectedIndices] = useState([]) @@ -65,13 +68,10 @@ export const SubmissionsContent = ({ setIsDeleteLoading(false) } - const totalSelected = useMemo( - () => - selectedIndices.length === results?.length - ? totalResults - : selectedIndices.length, - [results?.length, selectedIndices.length, totalResults] - ) + const totalSelected = + selectedIndices.length > 0 && selectedIndices.length === results?.length + ? totalResults - (totalHiddenResults ?? 0) + : selectedIndices.length const handleScrolledToBottom = useCallback( () => setSize((state) => state + 1), @@ -111,6 +111,12 @@ export const SubmissionsContent = ({ return ( + {totalHiddenResults && ( + + )} { @@ -27,6 +28,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { typebotId, typebot: { ownerId: user.id }, answers: { some: {} }, + isCompleted: isFreePlan(user), }, orderBy: { createdAt: 'desc', diff --git a/apps/builder/pages/api/typebots/[typebotId]/results/stats.ts b/apps/builder/pages/api/typebots/[typebotId]/results/stats.ts index 7b527b9f33..d9c2437a88 100644 --- a/apps/builder/pages/api/typebots/[typebotId]/results/stats.ts +++ b/apps/builder/pages/api/typebots/[typebotId]/results/stats.ts @@ -38,7 +38,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const stats: Stats = { totalViews, totalStarts, - completionRate: Math.round((totalCompleted / totalStarts) * 100), + totalCompleted, } return res.status(200).send({ stats }) } diff --git a/apps/builder/playwright/tests/results.spec.ts b/apps/builder/playwright/tests/results.spec.ts index 5aeb19384f..cd024b374d 100644 --- a/apps/builder/playwright/tests/results.spec.ts +++ b/apps/builder/playwright/tests/results.spec.ts @@ -2,6 +2,7 @@ import test, { expect, Page } from '@playwright/test' import { readFileSync } from 'fs' import { defaultTextInputOptions, InputStepType } from 'models' import { parse } from 'papaparse' +import path from 'path' import { generate } from 'short-uuid' import { createResults, @@ -86,6 +87,17 @@ test.describe('Results page', () => { const { data: dataAll } = parse(fileAll) validateExportAll(dataAll) }) + + test.describe('Free user', () => { + test.use({ + storageState: path.join(__dirname, '../freeUser.json'), + }) + test("Incomplete results shouldn't be displayed", async ({ page }) => { + await page.goto(`/typebots/${typebotId}/results`) + await page.click('text=Unlock 200 results') + await expect(page.locator('text=Upgrade now')).toBeVisible() + }) + }) }) const validateExportSelection = (data: unknown[]) => { diff --git a/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx b/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx index 1e04f74775..6742a8895c 100644 --- a/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx +++ b/packages/bot-engine/src/components/ChatBlock/ChatBlock.tsx @@ -92,10 +92,10 @@ export const ChatBlock = ({ const isSingleChoiceStep = isChoiceInput(currentStep) && !currentStep.options.isMultipleChoice if (isSingleChoiceStep) { - onBlockEnd( - currentStep.items.find((i) => i.content === answerContent) - ?.outgoingEdgeId - ) + const nextEdgeId = currentStep.items.find( + (i) => i.content === answerContent + )?.outgoingEdgeId + if (nextEdgeId) return onBlockEnd(nextEdgeId) } if (currentStep?.outgoingEdgeId || displayedSteps.length === steps.length) diff --git a/packages/models/src/answer.ts b/packages/models/src/answer.ts index e4917a6ec7..f8d86d4c77 100644 --- a/packages/models/src/answer.ts +++ b/packages/models/src/answer.ts @@ -5,5 +5,5 @@ export type Answer = Omit export type Stats = { totalViews: number totalStarts: number - completionRate: number + totalCompleted: number }