From 30fdcff7375035239484ece48e9ea10df9eb7d93 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Tue, 22 Jul 2025 17:34:26 +0530 Subject: [PATCH] feat: reset survey (#6267) --- .../context/environment-context.tsx | 2 + .../(analysis)/responses/page.test.tsx | 8 + .../[surveyId]/(analysis)/responses/page.tsx | 3 + .../[surveyId]/(analysis)/summary/actions.ts | 56 +++ .../components/SurveyAnalysisCTA.test.tsx | 346 ++++++++++++++++-- .../summary/components/SurveyAnalysisCTA.tsx | 50 ++- .../(analysis)/summary/lib/survey.test.ts | 83 +++++ .../(analysis)/summary/lib/survey.ts | 36 ++ .../[surveyId]/(analysis)/summary/page.tsx | 1 + apps/web/locales/de-DE.json | 4 + apps/web/locales/en-US.json | 4 + apps/web/locales/fr-FR.json | 4 + apps/web/locales/pt-BR.json | 4 + apps/web/locales/pt-PT.json | 4 + apps/web/locales/zh-Hant-TW.json | 4 + 15 files changed, 581 insertions(+), 28 deletions(-) create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/survey.test.ts create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/survey.ts diff --git a/apps/web/app/(app)/environments/[environmentId]/context/environment-context.tsx b/apps/web/app/(app)/environments/[environmentId]/context/environment-context.tsx index f5f4fbe0f2..0bc15edbbe 100644 --- a/apps/web/app/(app)/environments/[environmentId]/context/environment-context.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/context/environment-context.tsx @@ -7,6 +7,7 @@ import { TProject } from "@formbricks/types/project"; export interface EnvironmentContextType { environment: TEnvironment; project: TProject; + organizationId: string; } const EnvironmentContext = createContext(null); @@ -35,6 +36,7 @@ export const EnvironmentContextWrapper = ({ () => ({ environment, project, + organizationId: project.organizationId, }), [environment, project] ); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.test.tsx index f223f3ad3a..d5c71848b7 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.test.tsx @@ -3,6 +3,7 @@ import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentI import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage"; import Page from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page"; import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA"; +import { getDisplayCountBySurveyId } from "@/lib/display/service"; import { getPublicDomain } from "@/lib/getPublicUrl"; import { getResponseCountBySurveyId } from "@/lib/response/service"; import { getSurvey } from "@/lib/survey/service"; @@ -73,6 +74,10 @@ vi.mock("@/lib/response/service", () => ({ getResponseCountBySurveyId: vi.fn(), })); +vi.mock("@/lib/display/service", () => ({ + getDisplayCountBySurveyId: vi.fn(), +})); + vi.mock("@/lib/survey/service", () => ({ getSurvey: vi.fn(), })); @@ -178,6 +183,7 @@ describe("ResponsesPage", () => { vi.mocked(getUser).mockResolvedValue(mockUser); vi.mocked(getTagsByEnvironmentId).mockResolvedValue(mockTags); vi.mocked(getResponseCountBySurveyId).mockResolvedValue(10); + vi.mocked(getDisplayCountBySurveyId).mockResolvedValue(5); vi.mocked(findMatchingLocale).mockResolvedValue(mockLocale); vi.mocked(getPublicDomain).mockReturnValue(mockPublicDomain); }); @@ -206,6 +212,8 @@ describe("ResponsesPage", () => { isReadOnly: false, user: mockUser, publicDomain: mockPublicDomain, + responseCount: 10, + displayCount: 5, }), undefined ); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx index 28ef8a3452..ac269ef661 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx @@ -2,6 +2,7 @@ import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentI import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage"; import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA"; import { IS_FORMBRICKS_CLOUD, RESPONSES_PER_PAGE } from "@/lib/constants"; +import { getDisplayCountBySurveyId } from "@/lib/display/service"; import { getPublicDomain } from "@/lib/getPublicUrl"; import { getResponseCountBySurveyId } from "@/lib/response/service"; import { getSurvey } from "@/lib/survey/service"; @@ -40,6 +41,7 @@ const Page = async (props) => { // Get response count for the CTA component const responseCount = await getResponseCountBySurveyId(params.surveyId); + const displayCount = await getDisplayCountBySurveyId(params.surveyId); const locale = await findMatchingLocale(); const publicDomain = getPublicDomain(); @@ -56,6 +58,7 @@ const Page = async (props) => { user={user} publicDomain={publicDomain} responseCount={responseCount} + displayCount={displayCount} segments={segments} isContactsEnabled={isContactsEnabled} isFormbricksCloud={IS_FORMBRICKS_CLOUD} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts index e2a9ec2391..47e6eb7598 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts @@ -18,6 +18,7 @@ import { customAlphabet } from "nanoid"; import { z } from "zod"; import { ZId } from "@formbricks/types/common"; import { OperationNotAllowedError, ResourceNotFoundError, UnknownError } from "@formbricks/types/errors"; +import { deleteResponsesAndDisplaysForSurvey } from "./lib/survey"; const ZSendEmbedSurveyPreviewEmailAction = z.object({ surveyId: ZId, @@ -202,6 +203,61 @@ export const deleteResultShareUrlAction = authenticatedActionClient ) ); +const ZResetSurveyAction = z.object({ + surveyId: ZId, + organizationId: ZId, + projectId: ZId, +}); + +export const resetSurveyAction = authenticatedActionClient.schema(ZResetSurveyAction).action( + withAuditLogging( + "updated", + "survey", + async ({ + ctx, + parsedInput, + }: { + ctx: AuthenticatedActionClientCtx; + parsedInput: z.infer; + }) => { + await checkAuthorizationUpdated({ + userId: ctx.user.id, + organizationId: parsedInput.organizationId, + access: [ + { + type: "organization", + roles: ["owner", "manager"], + }, + { + type: "projectTeam", + minPermission: "readWrite", + projectId: parsedInput.projectId, + }, + ], + }); + + ctx.auditLoggingCtx.organizationId = parsedInput.organizationId; + ctx.auditLoggingCtx.surveyId = parsedInput.surveyId; + ctx.auditLoggingCtx.oldObject = null; + + const { deletedResponsesCount, deletedDisplaysCount } = await deleteResponsesAndDisplaysForSurvey( + parsedInput.surveyId + ); + + ctx.auditLoggingCtx.newObject = { + deletedResponsesCount: deletedResponsesCount, + deletedDisplaysCount: deletedDisplaysCount, + }; + + return { + success: true, + deletedResponsesCount: deletedResponsesCount, + deletedDisplaysCount: deletedDisplaysCount, + }; + } + ) +); + const ZGetEmailHtmlAction = z.object({ surveyId: ZId, }); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.test.tsx index e1d407eb56..5f03cddf57 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA.test.tsx @@ -33,6 +33,18 @@ vi.mock("@tolgee/react", () => ({ if (key === "environments.surveys.edit.caution_edit_duplicate") { return "Duplicate & Edit"; } + if (key === "environments.surveys.summary.reset_survey") { + return "Reset survey"; + } + if (key === "environments.surveys.summary.delete_all_existing_responses_and_displays") { + return "Delete all existing responses and displays"; + } + if (key === "environments.surveys.summary.reset_survey_warning") { + return "Resetting a survey removes all responses and metadata of this survey. This cannot be undone."; + } + if (key === "environments.surveys.summary.survey_reset_successfully") { + return "Survey reset successfully! 5 responses and 3 displays were deleted."; + } return key; }, }), @@ -40,12 +52,14 @@ vi.mock("@tolgee/react", () => ({ // Mock Next.js hooks const mockPush = vi.fn(); +const mockRefresh = vi.fn(); const mockPathname = "/environments/test-env-id/surveys/test-survey-id/summary"; const mockSearchParams = new URLSearchParams(); vi.mock("next/navigation", () => ({ useRouter: () => ({ push: mockPush, + refresh: mockRefresh, }), usePathname: () => mockPathname, useSearchParams: () => mockSearchParams, @@ -69,6 +83,10 @@ vi.mock("@/modules/survey/list/actions", () => ({ copySurveyToOtherEnvironmentAction: vi.fn(), })); +vi.mock("../actions", () => ({ + resetSurveyAction: vi.fn(), +})); + // Mock the useSingleUseId hook vi.mock("@/modules/survey/hooks/useSingleUseId", () => ({ useSingleUseId: vi.fn(() => ({ @@ -147,6 +165,34 @@ vi.mock("@/modules/ui/components/badge", () => ({ ), })); +vi.mock("@/modules/ui/components/confirmation-modal", () => ({ + ConfirmationModal: ({ + open, + setOpen, + title, + text, + buttonText, + onConfirm, + buttonVariant, + buttonLoading, + }: any) => ( +
+
{title}
+
{text}
+ + +
+ ), +})); + vi.mock("@/modules/ui/components/button", () => ({ Button: ({ children, onClick, className }: any) => (