From 3c3798ee98ee83f9295f1c1083279a3efffaacdc Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Mon, 5 Aug 2024 14:54:01 +0530 Subject: [PATCH] feat: Apply filters from question summary (#2940) Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com> Co-authored-by: Johannes --- .../summary/components/ConsentSummary.tsx | 94 ++++++++++++------- .../components/MatrixQuestionSummary.tsx | 26 ++++- .../components/MultipleChoiceSummary.tsx | 35 ++++++- .../summary/components/NPSSummary.tsx | 93 ++++++++++++------ .../components/PictureChoiceSummary.tsx | 30 +++++- .../summary/components/RatingSummary.tsx | 34 ++++++- .../summary/components/SummaryList.tsx | 75 ++++++++++++++- .../summary/components/SummaryPage.tsx | 7 +- .../(analysis)/summary/lib/utils.ts | 19 ++++ .../components/QuestionFilterComboBox.tsx | 22 ++--- .../[surveyId]/components/ResponseFilter.tsx | 18 ++-- apps/web/app/lib/surveys/surveys.ts | 17 +++- packages/lib/response/utils.ts | 17 +++- packages/types/responses.ts | 2 +- 14 files changed, 380 insertions(+), 109 deletions(-) diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.tsx index 9188a198a3..d441e94929 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary.tsx @@ -1,5 +1,10 @@ import { TAttributeClass } from "@formbricks/types/attribute-classes"; -import { TSurvey, TSurveyQuestionSummaryConsent } from "@formbricks/types/surveys/types"; +import { + TI18nString, + TSurvey, + TSurveyQuestionSummaryConsent, + TSurveyQuestionTypeEnum, +} from "@formbricks/types/surveys/types"; import { ProgressBar } from "@formbricks/ui/ProgressBar"; import { convertFloatToNDecimal } from "../lib/utils"; import { QuestionSummaryHeader } from "./QuestionSummaryHeader"; @@ -8,9 +13,33 @@ interface ConsentSummaryProps { questionSummary: TSurveyQuestionSummaryConsent; survey: TSurvey; attributeClasses: TAttributeClass[]; + setFilter: ( + questionId: string, + label: TI18nString, + questionType: TSurveyQuestionTypeEnum, + filterValue: string, + filterComboBoxValue?: string | string[] + ) => void; } -export const ConsentSummary = ({ questionSummary, survey, attributeClasses }: ConsentSummaryProps) => { +export const ConsentSummary = ({ + questionSummary, + survey, + attributeClasses, + setFilter, +}: ConsentSummaryProps) => { + const summaryItems = [ + { + title: "Accepted", + percentage: questionSummary.accepted.percentage, + count: questionSummary.accepted.count, + }, + { + title: "Dismissed", + percentage: questionSummary.dismissed.percentage, + count: questionSummary.dismissed.count, + }, + ]; return (
-
-
-
-

Accepted

-
-

- {convertFloatToNDecimal(questionSummary.accepted.percentage, 1)}% + {summaryItems.map((summaryItem) => { + return ( +

+ setFilter( + questionSummary.question.id, + questionSummary.question.headline, + questionSummary.question.type, + "is", + summaryItem.title + ) + }> +
+
+

+ {summaryItem.title} +

+
+

+ {convertFloatToNDecimal(summaryItem.percentage, 1)}% +

+
+
+

+ {summaryItem.count} {summaryItem.count === 1 ? "response" : "responses"}

-
-

- {questionSummary.accepted.count}{" "} - {questionSummary.accepted.count === 1 ? "response" : "responses"} -

-
- -
-
-
-
-

Dismissed

-
-

- {convertFloatToNDecimal(questionSummary.dismissed.percentage, 1)}% -

+
+
-

- {questionSummary.dismissed.count}{" "} - {questionSummary.dismissed.count === 1 ? "response" : "responses"} -

-
- -
+ ); + })}
); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary.tsx index bfa7003a1b..7536670927 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary.tsx @@ -1,5 +1,10 @@ import { TAttributeClass } from "@formbricks/types/attribute-classes"; -import { TSurvey, TSurveyQuestionSummaryMatrix } from "@formbricks/types/surveys/types"; +import { + TI18nString, + TSurvey, + TSurveyQuestionSummaryMatrix, + TSurveyQuestionTypeEnum, +} from "@formbricks/types/surveys/types"; import { TooltipRenderer } from "@formbricks/ui/Tooltip"; import { QuestionSummaryHeader } from "./QuestionSummaryHeader"; @@ -7,12 +12,20 @@ interface MatrixQuestionSummaryProps { questionSummary: TSurveyQuestionSummaryMatrix; survey: TSurvey; attributeClasses: TAttributeClass[]; + setFilter: ( + questionId: string, + label: TI18nString, + questionType: TSurveyQuestionTypeEnum, + filterValue: string, + filterComboBoxValue?: string | string[] + ) => void; } export const MatrixQuestionSummary = ({ questionSummary, survey, attributeClasses, + setFilter, }: MatrixQuestionSummaryProps) => { const getOpacityLevel = (percentage: number): string => { const parsedPercentage = percentage; @@ -74,7 +87,16 @@ export const MatrixQuestionSummary = ({ )}>
+ className="hover:outline-brand-dark m-1 flex h-full w-40 cursor-pointer items-center justify-center rounded p-4 text-sm text-slate-950 hover:outline" + onClick={() => + setFilter( + questionSummary.question.id, + questionSummary.question.headline, + questionSummary.question.type, + rowLabel, + column + ) + }> {percentage}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx index 869026d5b8..a03a3368e4 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx @@ -2,7 +2,13 @@ import Link from "next/link"; import { useState } from "react"; import { getPersonIdentifier } from "@formbricks/lib/person/utils"; import { TAttributeClass } from "@formbricks/types/attribute-classes"; -import { TSurvey, TSurveyQuestionSummaryMultipleChoice, TSurveyType } from "@formbricks/types/surveys/types"; +import { + TI18nString, + TSurvey, + TSurveyQuestionSummaryMultipleChoice, + TSurveyQuestionTypeEnum, + TSurveyType, +} from "@formbricks/types/surveys/types"; import { PersonAvatar } from "@formbricks/ui/Avatars"; import { Button } from "@formbricks/ui/Button"; import { ProgressBar } from "@formbricks/ui/ProgressBar"; @@ -15,6 +21,13 @@ interface MultipleChoiceSummaryProps { surveyType: TSurveyType; survey: TSurvey; attributeClasses: TAttributeClass[]; + setFilter: ( + questionId: string, + label: TI18nString, + questionType: TSurveyQuestionTypeEnum, + filterValue: string, + filterComboBoxValue?: string | string[] + ) => void; } export const MultipleChoiceSummary = ({ @@ -23,6 +36,7 @@ export const MultipleChoiceSummary = ({ surveyType, survey, attributeClasses, + setFilter, }: MultipleChoiceSummaryProps) => { const [visibleOtherResponses, setVisibleOtherResponses] = useState(10); @@ -55,10 +69,21 @@ export const MultipleChoiceSummary = ({ />
{results.map((result, resultsIdx) => ( -
+
+ setFilter( + questionSummary.question.id, + questionSummary.question.headline, + questionSummary.question.type, + questionSummary.type === "multipleChoiceSingle" ? "Includes either" : "Includes all", + [result.value] + ) + }>
-

+

{results.length - resultsIdx} - {result.value}

@@ -71,7 +96,9 @@ export const MultipleChoiceSummary = ({ {result.count} {result.count === 1 ? "response" : "responses"}

- +
+ +
{result.others && result.others.length > 0 && (
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.tsx index bffeb3de37..c6f91b2876 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary.tsx @@ -1,5 +1,10 @@ import { TAttributeClass } from "@formbricks/types/attribute-classes"; -import { TSurvey, TSurveyQuestionSummaryNps } from "@formbricks/types/surveys/types"; +import { + TI18nString, + TSurvey, + TSurveyQuestionSummaryNps, + TSurveyQuestionTypeEnum, +} from "@formbricks/types/surveys/types"; import { HalfCircle, ProgressBar } from "@formbricks/ui/ProgressBar"; import { convertFloatToNDecimal } from "../lib/utils"; import { QuestionSummaryHeader } from "./QuestionSummaryHeader"; @@ -8,9 +13,49 @@ interface NPSSummaryProps { questionSummary: TSurveyQuestionSummaryNps; survey: TSurvey; attributeClasses: TAttributeClass[]; + setFilter: ( + questionId: string, + label: TI18nString, + questionType: TSurveyQuestionTypeEnum, + filterValue: string, + filterComboBoxValue?: string | string[] + ) => void; } -export const NPSSummary = ({ questionSummary, survey, attributeClasses }: NPSSummaryProps) => { +export const NPSSummary = ({ questionSummary, survey, attributeClasses, setFilter }: NPSSummaryProps) => { + const applyFilter = (group: string) => { + const filters = { + promoters: { + comparison: "Includes either", + values: ["9", "10"], + }, + passives: { + comparison: "Includes either", + values: ["7", "8"], + }, + detractors: { + comparison: "Is less than", + values: "7", + }, + dismissed: { + comparison: "Skipped", + values: undefined, + }, + }; + + const filter = filters[group]; + + if (filter) { + setFilter( + questionSummary.question.id, + questionSummary.question.headline, + questionSummary.question.type, + filter.comparison, + filter.values + ); + } + }; + return (
- {["promoters", "passives", "detractors"].map((group) => ( -
-
+ {["promoters", "passives", "detractors", "dismissed"].map((group) => ( +
applyFilter(group)}> +
-

{group}

+

+ {group} +

- {convertFloatToNDecimal(questionSummary[group].percentage, 1)}% + {convertFloatToNDecimal(questionSummary[group]?.percentage, 1)}%

- {questionSummary[group].count} {questionSummary[group].count === 1 ? "response" : "responses"} + {questionSummary[group]?.count}{" "} + {questionSummary[group]?.count === 1 ? "response" : "responses"}

- +
))}
- {questionSummary.dismissed?.count > 0 && ( -
-
-
-
-

dismissed

-
-

- {convertFloatToNDecimal(questionSummary.dismissed.percentage, 1)}% -

-
-
-

- {questionSummary.dismissed.count}{" "} - {questionSummary.dismissed.count === 1 ? "response" : "responses"} -

-
- -
-
- )} +
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.tsx index 10d7cbe8ef..bf2306ff1b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary.tsx @@ -1,6 +1,11 @@ import Image from "next/image"; import { TAttributeClass } from "@formbricks/types/attribute-classes"; -import { TSurvey, TSurveyQuestionSummaryPictureSelection } from "@formbricks/types/surveys/types"; +import { + TI18nString, + TSurvey, + TSurveyQuestionSummaryPictureSelection, + TSurveyQuestionTypeEnum, +} from "@formbricks/types/surveys/types"; import { ProgressBar } from "@formbricks/ui/ProgressBar"; import { convertFloatToNDecimal } from "../lib/utils"; import { QuestionSummaryHeader } from "./QuestionSummaryHeader"; @@ -9,12 +14,20 @@ interface PictureChoiceSummaryProps { questionSummary: TSurveyQuestionSummaryPictureSelection; survey: TSurvey; attributeClasses: TAttributeClass[]; + setFilter: ( + questionId: string, + label: TI18nString, + questionType: TSurveyQuestionTypeEnum, + filterValue: string, + filterComboBoxValue?: string | string[] + ) => void; } export const PictureChoiceSummary = ({ questionSummary, survey, attributeClasses, + setFilter, }: PictureChoiceSummaryProps) => { const results = questionSummary.choices; @@ -26,8 +39,19 @@ export const PictureChoiceSummary = ({ attributeClasses={attributeClasses} />
- {results.map((result) => ( -
+ {results.map((result, index) => ( +
+ setFilter( + questionSummary.question.id, + questionSummary.question.headline, + questionSummary.question.type, + "Includes all", + [`Picture ${index + 1}`] + ) + }>
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.tsx index 4e6b33d799..b16f8bca91 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary.tsx @@ -2,7 +2,12 @@ import { convertFloatToNDecimal } from "@/app/(app)/environments/[environmentId] import { CircleSlash2, SmileIcon, StarIcon } from "lucide-react"; import { useMemo } from "react"; import { TAttributeClass } from "@formbricks/types/attribute-classes"; -import { TSurvey, TSurveyQuestionSummaryRating } from "@formbricks/types/surveys/types"; +import { + TI18nString, + TSurvey, + TSurveyQuestionSummaryRating, + TSurveyQuestionTypeEnum, +} from "@formbricks/types/surveys/types"; import { ProgressBar } from "@formbricks/ui/ProgressBar"; import { RatingResponse } from "@formbricks/ui/RatingResponse"; import { QuestionSummaryHeader } from "./QuestionSummaryHeader"; @@ -11,9 +16,21 @@ interface RatingSummaryProps { questionSummary: TSurveyQuestionSummaryRating; survey: TSurvey; attributeClasses: TAttributeClass[]; + setFilter: ( + questionId: string, + label: TI18nString, + questionType: TSurveyQuestionTypeEnum, + filterValue: string, + filterComboBoxValue?: string | string[] + ) => void; } -export const RatingSummary = ({ questionSummary, survey, attributeClasses }: RatingSummaryProps) => { +export const RatingSummary = ({ + questionSummary, + survey, + attributeClasses, + setFilter, +}: RatingSummaryProps) => { const getIconBasedOnScale = useMemo(() => { const scale = questionSummary.question.scale; if (scale === "number") return ; @@ -36,7 +53,18 @@ export const RatingSummary = ({ questionSummary, survey, attributeClasses }: Rat />
{questionSummary.choices.map((result) => ( -
+
+ setFilter( + questionSummary.question.id, + questionSummary.question.headline, + questionSummary.question.type, + "Is equal to", + result.rating.toString() + ) + }>
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx index 4689bac4e8..4600d04870 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx @@ -1,3 +1,9 @@ +"use client"; + +import { + SelectedFilterValue, + useResponseFilter, +} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext"; import { EmptyAppSurveys } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys"; import { CTASummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CTASummary"; import { CalSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary"; @@ -11,9 +17,13 @@ import { NPSSummary } from "@/app/(app)/environments/[environmentId]/surveys/[su import { OpenTextSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary"; import { PictureChoiceSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary"; import { RatingSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary"; +import { constructToastMessage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils"; +import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox"; +import { toast } from "react-hot-toast"; +import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { TAttributeClass } from "@formbricks/types/attribute-classes"; import { TEnvironment } from "@formbricks/types/environment"; -import { TSurveySummary } from "@formbricks/types/surveys/types"; +import { TI18nString, TSurveySummary } from "@formbricks/types/surveys/types"; import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; import { TSurvey } from "@formbricks/types/surveys/types"; import { EmptySpaceFiller } from "@formbricks/ui/EmptySpaceFiller"; @@ -25,7 +35,6 @@ interface SummaryListProps { responseCount: number | null; environment: TEnvironment; survey: TSurvey; - fetchingSummary: boolean; totalResponseCount: number; attributeClasses: TAttributeClass[]; } @@ -35,20 +44,72 @@ export const SummaryList = ({ environment, responseCount, survey, - fetchingSummary, totalResponseCount, attributeClasses, }: SummaryListProps) => { + const { setSelectedFilter, selectedFilter } = useResponseFilter(); const widgetSetupCompleted = survey.type === "app" ? environment.appSetupCompleted : environment.websiteSetupCompleted; + const setFilter = ( + questionId: string, + label: TI18nString, + questionType: TSurveyQuestionTypeEnum, + filterValue: string, + filterComboBoxValue?: string | string[] + ) => { + const filterObject: SelectedFilterValue = { ...selectedFilter }; + const value = { + id: questionId, + label: getLocalizedValue(label, "default"), + questionType: questionType, + type: OptionsType.QUESTIONS, + }; + + // Find the index of the existing filter with the same questionId + const existingFilterIndex = filterObject.filter.findIndex( + (filter) => filter.questionType.id === questionId + ); + + if (existingFilterIndex !== -1) { + // Replace the existing filter + filterObject.filter[existingFilterIndex] = { + questionType: value, + filterType: { + filterComboBoxValue: filterComboBoxValue, + filterValue: filterValue, + }, + }; + toast.success("Filter updated successfully", { duration: 5000 }); + } else { + // Add new filter + filterObject.filter.push({ + questionType: value, + filterType: { + filterComboBoxValue: filterComboBoxValue, + filterValue: filterValue, + }, + }); + toast.success( + constructToastMessage(questionType, filterValue, survey, questionId, filterComboBoxValue) ?? + "Filter added successfully", + { duration: 5000 } + ); + } + + setSelectedFilter({ + filter: [...filterObject.filter], + onlyComplete: filterObject.onlyComplete, + }); + }; + return (
{(survey.type === "app" || survey.type === "website") && responseCount === 0 && !widgetSetupCompleted ? ( - ) : fetchingSummary ? ( + ) : summary.length === 0 ? ( ) : responseCount === 0 ? ( ); } @@ -93,6 +155,7 @@ export const SummaryList = ({ questionSummary={questionSummary} survey={survey} attributeClasses={attributeClasses} + setFilter={setFilter} /> ); } @@ -113,6 +176,7 @@ export const SummaryList = ({ questionSummary={questionSummary} survey={survey} attributeClasses={attributeClasses} + setFilter={setFilter} /> ); } @@ -123,6 +187,7 @@ export const SummaryList = ({ questionSummary={questionSummary} survey={survey} attributeClasses={attributeClasses} + setFilter={setFilter} /> ); } @@ -133,6 +198,7 @@ export const SummaryList = ({ questionSummary={questionSummary} survey={survey} attributeClasses={attributeClasses} + setFilter={setFilter} /> ); } @@ -176,6 +242,7 @@ export const SummaryList = ({ questionSummary={questionSummary} survey={survey} attributeClasses={attributeClasses} + setFilter={setFilter} /> ); } diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx index d23ceb1f21..6c0a0b5dda 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx @@ -64,7 +64,6 @@ export const SummaryPage = ({ const [responseCount, setResponseCount] = useState(null); const [surveySummary, setSurveySummary] = useState(initialSurveySummary); const [showDropOffs, setShowDropOffs] = useState(false); - const [isFetchingSummary, setFetchingSummary] = useState(true); const { selectedFilter, dateRange, resetState } = useResponseFilter(); @@ -78,7 +77,6 @@ export const SummaryPage = ({ useEffect(() => { const handleInitialData = async () => { try { - setFetchingSummary(true); let updatedResponseCount; if (isSharingPage) { updatedResponseCount = await getResponseCountBySurveySharingKeyAction(sharingKey, filters); @@ -95,8 +93,8 @@ export const SummaryPage = ({ } setSurveySummary(updatedSurveySummary); - } finally { - setFetchingSummary(false); + } catch (error) { + console.error(error); } }; @@ -132,7 +130,6 @@ export const SummaryPage = ({ responseCount={responseCount} survey={surveyMemoized} environment={environment} - fetchingSummary={isFetchingSummary} totalResponseCount={totalResponseCount} attributeClasses={attributeClasses} /> diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils.ts index e1c35efc62..044b038339 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils.ts @@ -1,3 +1,22 @@ +import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; + export const convertFloatToNDecimal = (num: number, N: number = 2) => { return Math.round(num * Math.pow(10, N)) / Math.pow(10, N); }; + +export const constructToastMessage = ( + questionType: TSurveyQuestionTypeEnum, + filterValue: string, + survey: TSurvey, + questionId: string, + filterComboBoxValue?: string | string[] +) => { + const questionIdx = survey.questions.findIndex((question) => question.id === questionId); + if (questionType === "matrix") { + return `Added filter for responses where answer to question ${questionIdx + 1} is ${filterComboBoxValue} - ${filterValue}`; + } else if (filterComboBoxValue === undefined) { + return `Added filter for responses where answer to question ${questionIdx + 1} is skipped`; + } else { + return `Added filter for responses where answer to question ${questionIdx + 1} ${filterValue} ${Array.isArray(filterComboBoxValue) ? filterComboBoxValue.join(",") : filterComboBoxValue}`; + } +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.tsx index a356c0ec0b..38701656c9 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.tsx @@ -120,7 +120,7 @@ export const QuestionFilterComboBox = ({ disabled || isDisabledComboBox || !filterValue ? "opacity-50" : "cursor-pointer" )}> {filterComboBoxValue && filterComboBoxValue?.length > 0 ? ( - !isMultiple ? ( + !Array.isArray(filterComboBoxValue) ? (

{filterComboBoxValue}

) : (
@@ -156,18 +156,14 @@ export const QuestionFilterComboBox = ({ {options?.map((o) => ( { - !isMultiple - ? onChangeFilterComboBoxValue( - typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o - ) - : onChangeFilterComboBoxValue( - Array.isArray(filterComboBoxValue) - ? [ - ...filterComboBoxValue, - typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o, - ] - : [typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o] - ); + onChangeFilterComboBoxValue( + Array.isArray(filterComboBoxValue) + ? [ + ...filterComboBoxValue, + typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o, + ] + : [typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o] + ); !isMultiple && setOpen(false); }} className="cursor-pointer"> diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter.tsx index 693e6aa230..b9a893755f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter.tsx @@ -9,9 +9,7 @@ import { QuestionFilterComboBox } from "@/app/(app)/environments/[environmentId] import { generateQuestionAndFilterOptions } from "@/app/lib/surveys/surveys"; import { getSurveyFilterDataBySurveySharingKeyAction } from "@/app/share/[sharingKey]/actions"; import clsx from "clsx"; -import { isEqual } from "lodash"; -import { TrashIcon } from "lucide-react"; -import { ChevronDown, ChevronUp, Plus } from "lucide-react"; +import { ChevronDown, ChevronUp, Plus, TrashIcon } from "lucide-react"; import { useParams } from "next/navigation"; import { useEffect, useState } from "react"; import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; @@ -174,9 +172,7 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => { const handleApplyFilters = () => { clearItem(); - if (!isEqual(filterValue, selectedFilter)) { - setSelectedFilter(filterValue); - } + setSelectedFilter(filterValue); setIsOpen(false); }; @@ -187,10 +183,16 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => { setIsOpen(open); }; + useEffect(() => { + setFilterValue(selectedFilter); + }, [selectedFilter]); + return ( - Filter {filterValue.filter.length > 0 && `(${filterValue.filter.length})`} + + Filter {filterValue.filter.length > 0 && `(${filterValue.filter.length})`} +
{isOpen ? ( @@ -203,7 +205,7 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => { align="start" className="w-[300px] border-slate-200 bg-slate-100 p-6 sm:w-[400px] md:w-[750px] lg:w-[1000px]">
-

Show all responses that match

+

Show all responses that match

Show all responses where...

diff --git a/apps/web/app/lib/surveys/surveys.ts b/apps/web/app/lib/surveys/surveys.ts index 3c38f67e8f..f1fa077f53 100644 --- a/apps/web/app/lib/surveys/surveys.ts +++ b/apps/web/app/lib/surveys/surveys.ts @@ -15,15 +15,14 @@ import { TSurveyMetaFieldFilter, TSurveyPersonAttributes, } from "@formbricks/types/responses"; -import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; -import { TSurvey } from "@formbricks/types/surveys/types"; +import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; import { TTag } from "@formbricks/types/tags"; const conditionOptions = { openText: ["is"], multipleChoiceSingle: ["Includes either"], multipleChoiceMulti: ["Includes all", "Includes either"], - nps: ["Is equal to", "Is less than", "Is more than", "Submitted", "Skipped"], + nps: ["Is equal to", "Is less than", "Is more than", "Submitted", "Skipped", "Includes either"], rating: ["Is equal to", "Is less than", "Is more than", "Submitted", "Skipped"], cta: ["is"], tags: ["is"], @@ -278,6 +277,7 @@ export const getFormattedFilters = ( op: "skipped", }; } + break; } case TSurveyQuestionTypeEnum.MultipleChoiceSingle: case TSurveyQuestionTypeEnum.MultipleChoiceMulti: { @@ -292,6 +292,7 @@ export const getFormattedFilters = ( value: filterType.filterComboBoxValue as string[], }; } + break; } case TSurveyQuestionTypeEnum.NPS: case TSurveyQuestionTypeEnum.Rating: { @@ -318,7 +319,13 @@ export const getFormattedFilters = ( filters.data[questionType.id ?? ""] = { op: "skipped", }; + } else if (filterType.filterValue === "Includes either") { + filters.data[questionType.id ?? ""] = { + op: "includesOne", + value: (filterType.filterComboBoxValue as string[]).map((value) => parseInt(value)), + }; } + break; } case TSurveyQuestionTypeEnum.CTA: { if (filterType.filterComboBoxValue === "Clicked") { @@ -330,6 +337,7 @@ export const getFormattedFilters = ( op: "skipped", }; } + break; } case TSurveyQuestionTypeEnum.Consent: { if (filterType.filterComboBoxValue === "Accepted") { @@ -341,6 +349,7 @@ export const getFormattedFilters = ( op: "skipped", }; } + break; } case TSurveyQuestionTypeEnum.PictureSelection: { const questionId = questionType.id ?? ""; @@ -369,6 +378,7 @@ export const getFormattedFilters = ( value: selectedOptions, }; } + break; } case TSurveyQuestionTypeEnum.Matrix: { if ( @@ -381,6 +391,7 @@ export const getFormattedFilters = ( value: { [filterType.filterValue]: filterType.filterComboBoxValue }, }; } + break; } } }); diff --git a/packages/lib/response/utils.ts b/packages/lib/response/utils.ts index 302dd5ae09..41e5a070df 100644 --- a/packages/lib/response/utils.ts +++ b/packages/lib/response/utils.ts @@ -220,6 +220,12 @@ export const buildWhereClause = (filterCriteria?: TResponseFilterCriteria) => { equals: "dismissed", }, }, + { + data: { + path: [key], + equals: "", + }, + }, // For address question { data: { @@ -300,7 +306,7 @@ export const buildWhereClause = (filterCriteria?: TResponseFilterCriteria) => { break; case "includesOne": data.push({ - OR: val.value.map((value: string) => ({ + OR: val.value.map((value: string | number) => ({ OR: [ // for MultipleChoiceMulti { @@ -372,6 +378,15 @@ export const buildWhereClause = (filterCriteria?: TResponseFilterCriteria) => { }, }); break; + case "matrix": + const rowLabel = Object.keys(val.value)[0]; + data.push({ + data: { + path: [key, rowLabel], + equals: val.value[rowLabel], + }, + }); + break; } }); diff --git a/packages/types/responses.ts b/packages/types/responses.ts index f923864dd3..9036f08499 100644 --- a/packages/types/responses.ts +++ b/packages/types/responses.ts @@ -59,7 +59,7 @@ const ZResponseFilterCriteriaDataGreaterThan = z.object({ const ZResponseFilterCriteriaDataIncludesOne = z.object({ op: z.literal(ZSurveyLogicCondition.Values.includesOne), - value: z.array(z.string()), + value: z.union([z.array(z.string()), z.array(z.number())]), }); const ZResponseFilterCriteriaDataIncludesAll = z.object({