From f386e47efaee0d8ea5117563ea67873d6a5fa92c Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Mon, 19 Aug 2024 14:20:19 +0530 Subject: [PATCH] fix: Let CTA and consent question store nothing if dismissed (#2977) Co-authored-by: Matthias Nannt --- apps/web/app/s/[surveyId]/lib/prefilling.ts | 7 +- .../data-migration.ts | 105 ++++++++++++++++++ packages/database/package.json | 3 +- packages/lib/response/utils.ts | 8 +- packages/lib/utils/evaluateLogic.ts | 5 +- .../general/QuestionConditional.tsx | 2 +- .../src/components/questions/CTAQuestion.tsx | 4 +- .../components/questions/ConsentQuestion.tsx | 2 +- packages/surveys/src/lib/logicEvaluator.ts | 3 +- 9 files changed, 120 insertions(+), 19 deletions(-) create mode 100644 packages/database/data-migrations/20240807120500_cta_consent_dismissed_inconsistency/data-migration.ts diff --git a/apps/web/app/s/[surveyId]/lib/prefilling.ts b/apps/web/app/s/[surveyId]/lib/prefilling.ts index 28e5514064..4178b49e4b 100644 --- a/apps/web/app/s/[surveyId]/lib/prefilling.ts +++ b/apps/web/app/s/[surveyId]/lib/prefilling.ts @@ -104,9 +104,12 @@ export const transformAnswer = ( ): string | number | string[] => { switch (question.type) { case TSurveyQuestionTypeEnum.OpenText: - case TSurveyQuestionTypeEnum.MultipleChoiceSingle: + case TSurveyQuestionTypeEnum.MultipleChoiceSingle: { + return answer; + } case TSurveyQuestionTypeEnum.Consent: case TSurveyQuestionTypeEnum.CTA: { + if (answer === "dismissed") return ""; return answer; } @@ -143,6 +146,6 @@ export const transformAnswer = ( } default: - return "dismissed"; + return ""; } }; diff --git a/packages/database/data-migrations/20240807120500_cta_consent_dismissed_inconsistency/data-migration.ts b/packages/database/data-migrations/20240807120500_cta_consent_dismissed_inconsistency/data-migration.ts new file mode 100644 index 0000000000..e024a9a6a0 --- /dev/null +++ b/packages/database/data-migrations/20240807120500_cta_consent_dismissed_inconsistency/data-migration.ts @@ -0,0 +1,105 @@ +/* eslint-disable no-console -- logging is allowed in migration scripts */ +import { PrismaClient } from "@prisma/client"; +import { type TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; + +const prisma = new PrismaClient(); + +async function runMigration(): Promise { + await prisma.$transaction( + async (tx) => { + const startTime = Date.now(); + console.log("Starting data migration..."); + + // Get all surveys with status not in draft and questions containing cta or consent + const relevantSurveys = await tx.survey.findMany({ + where: { + status: { + notIn: ["draft"], + }, + OR: [ + { + questions: { + array_contains: [{ type: "cta" }], + }, + }, + { + questions: { + array_contains: [{ type: "consent" }], + }, + }, + ], + }, + select: { + id: true, + questions: true, + }, + }); + + // Process each survey + const migrationPromises = relevantSurveys.map(async (survey) => { + const ctaOrConsentQuestionIds = survey.questions + .filter( + (ques: TSurveyQuestion) => + ques.type === TSurveyQuestionTypeEnum.CTA || ques.type === TSurveyQuestionTypeEnum.Consent + ) + .map((ques: TSurveyQuestion) => ques.id); + + // Get all responses for this survey + const responses = await prisma.response.findMany({ + where: { + surveyId: survey.id, + }, + select: { + id: true, + data: true, + }, + }); + + // Update each response + return responses.map(async (response) => { + const updatedData = { ...response.data }; + + ctaOrConsentQuestionIds.forEach((questionId: string) => { + if (updatedData[questionId] && updatedData[questionId] === "dismissed") { + updatedData[questionId] = ""; + } + }); + + return prisma.response.update({ + where: { id: response.id }, + data: { data: updatedData }, + }); + }); + }); + + const migrationPromisesFlat = migrationPromises.flat(); + await Promise.all(migrationPromisesFlat); + + const endTime = Date.now(); + console.log(`Data migration completed. Total time: ${((endTime - startTime) / 1000).toString()}s`); + }, + { + timeout: 180000, // 3 minutes + } + ); +} + +function handleError(error: unknown): void { + console.error("An error occurred during migration:", error); + process.exit(1); +} + +function handleDisconnectError(): void { + console.error("Failed to disconnect Prisma client"); + process.exit(1); +} + +function main(): void { + runMigration() + .catch(handleError) + .finally(() => { + prisma.$disconnect().catch(handleDisconnectError); + }); +} + +main(); diff --git a/packages/database/package.json b/packages/database/package.json index af875bd0d7..b1f61c44ec 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -45,7 +45,8 @@ "data-migration:multiple-endings": "ts-node ./data-migrations/20240801120500_thankYouCard_to_endings/data-migration.ts", "data-migration:simplified-email-verification": "ts-node ./data-migrations/20240726124100_replace_verifyEmail_with_isVerifyEmailEnabled/data-migration.ts", "data-migration:fix-logic-end-destination": "ts-node ./data-migrations/20240806120500_fix-logic-end-destination/data-migration.ts", - "data-migration:v2.4": "pnpm data-migration:segments-cleanup && pnpm data-migration:multiple-endings && pnpm data-migration:simplified-email-verification && pnpm data-migration:fix-logic-end-destination" + "data-migration:v2.4": "pnpm data-migration:segments-cleanup && pnpm data-migration:multiple-endings && pnpm data-migration:simplified-email-verification && pnpm data-migration:fix-logic-end-destination", + "data-migration:remove-dismissed-value-inconsistency": "ts-node ./data-migrations/20240807120500_cta_consent_dismissed_inconsistency/data-migration.ts" }, "dependencies": { "@prisma/client": "^5.18.0", diff --git a/packages/lib/response/utils.ts b/packages/lib/response/utils.ts index 41e5a070df..e1543a17e7 100644 --- a/packages/lib/response/utils.ts +++ b/packages/lib/response/utils.ts @@ -205,7 +205,7 @@ export const buildWhereClause = (filterCriteria?: TResponseFilterCriteria) => { }, }); break; - case "skipped": // need to handle dismissed case for CTA type question, that would hinder other ques(eg open text) + case "skipped": data.push({ OR: [ { @@ -214,12 +214,6 @@ export const buildWhereClause = (filterCriteria?: TResponseFilterCriteria) => { equals: Prisma.DbNull, }, }, - { - data: { - path: [key], - equals: "dismissed", - }, - }, { data: { path: [key], diff --git a/packages/lib/utils/evaluateLogic.ts b/packages/lib/utils/evaluateLogic.ts index f17125090e..36256de5fc 100644 --- a/packages/lib/utils/evaluateLogic.ts +++ b/packages/lib/utils/evaluateLogic.ts @@ -35,7 +35,7 @@ export const evaluateCondition = (logic: TSurveyLogic, responseValue: any): bool return responseValue === "clicked"; case "submitted": if (typeof responseValue === "string") { - return responseValue !== "dismissed" && responseValue !== "" && responseValue !== null; + return responseValue !== "" && responseValue !== null; } else if (Array.isArray(responseValue)) { return responseValue.length > 0; } else if (typeof responseValue === "number") { @@ -46,8 +46,7 @@ export const evaluateCondition = (logic: TSurveyLogic, responseValue: any): bool return ( (Array.isArray(responseValue) && responseValue.length === 0) || responseValue === "" || - responseValue === null || - responseValue === "dismissed" + responseValue === null ); default: return false; diff --git a/packages/surveys/src/components/general/QuestionConditional.tsx b/packages/surveys/src/components/general/QuestionConditional.tsx index 6b47e07108..121f81e5a2 100644 --- a/packages/surveys/src/components/general/QuestionConditional.tsx +++ b/packages/surveys/src/components/general/QuestionConditional.tsx @@ -52,7 +52,7 @@ export const QuestionConditional = ({ autoFocusEnabled, currentQuestionId, }: QuestionConditionalProps) => { - if (!value && prefilledQuestionValue) { + if (!value && (prefilledQuestionValue || prefilledQuestionValue === "")) { if (skipPrefilled) { onSubmit({ [question.id]: prefilledQuestionValue }, { [question.id]: 0 }); } else { diff --git a/packages/surveys/src/components/questions/CTAQuestion.tsx b/packages/surveys/src/components/questions/CTAQuestion.tsx index 1d02e86ba6..e957d36ee3 100644 --- a/packages/surveys/src/components/questions/CTAQuestion.tsx +++ b/packages/surveys/src/components/questions/CTAQuestion.tsx @@ -78,8 +78,8 @@ export const CTAQuestion = ({ onClick={() => { const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime); setTtc(updatedTtcObj); - onSubmit({ [question.id]: "dismissed" }, updatedTtcObj); - onChange({ [question.id]: "dismissed" }); + onSubmit({ [question.id]: "" }, updatedTtcObj); + onChange({ [question.id]: "" }); }} className="fb-text-heading focus:fb-ring-focus fb-mr-4 fb-flex fb-items-center fb-rounded-md fb-px-3 fb-py-3 fb-text-base fb-font-medium fb-leading-4 hover:fb-opacity-90 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2"> {getLocalizedValue(question.dismissButtonLabel, languageCode) || "Skip"} diff --git a/packages/surveys/src/components/questions/ConsentQuestion.tsx b/packages/surveys/src/components/questions/ConsentQuestion.tsx index bbe95b0d93..544a4c7772 100644 --- a/packages/surveys/src/components/questions/ConsentQuestion.tsx +++ b/packages/surveys/src/components/questions/ConsentQuestion.tsx @@ -87,7 +87,7 @@ export const ConsentQuestion = ({ if (e.target instanceof HTMLInputElement && e.target.checked) { onChange({ [question.id]: "accepted" }); } else { - onChange({ [question.id]: "dismissed" }); + onChange({ [question.id]: "" }); } }} checked={value === "accepted"} diff --git a/packages/surveys/src/lib/logicEvaluator.ts b/packages/surveys/src/lib/logicEvaluator.ts index 79e564432b..d41c2e4639 100644 --- a/packages/surveys/src/lib/logicEvaluator.ts +++ b/packages/surveys/src/lib/logicEvaluator.ts @@ -42,7 +42,7 @@ export const evaluateCondition = ( return responseValue === "clicked"; case "submitted": if (typeof responseValue === "string") { - return responseValue !== "dismissed" && responseValue !== "" && responseValue !== null; + return responseValue !== "" && responseValue !== null; } else if (Array.isArray(responseValue)) { return responseValue.length > 0; } else if (typeof responseValue === "number") { @@ -55,7 +55,6 @@ export const evaluateCondition = ( responseValue === "" || responseValue === null || responseValue === undefined || - responseValue === "dismissed" || (isObject && Object.entries(responseValue).length === 0) ); case "uploaded":