feat: Apply filters from question summary (#2940)

Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
Dhruwang Jariwala
2024-08-05 14:54:01 +05:30
committed by GitHub
parent 8df722ab02
commit 3c3798ee98
14 changed files with 380 additions and 109 deletions

View File

@@ -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 (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader
@@ -19,40 +48,41 @@ export const ConsentSummary = ({ questionSummary, survey, attributeClasses }: Co
attributeClasses={attributeClasses}
/>
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
<div>
<div className="text flex justify-between px-2 pb-2">
<div className="mr-8 flex space-x-1">
<p className="font-semibold text-slate-700">Accepted</p>
<div>
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
{convertFloatToNDecimal(questionSummary.accepted.percentage, 1)}%
{summaryItems.map((summaryItem) => {
return (
<div
className="group cursor-pointer"
key={summaryItem.title}
onClick={() =>
setFilter(
questionSummary.question.id,
questionSummary.question.headline,
questionSummary.question.type,
"is",
summaryItem.title
)
}>
<div className="text flex justify-between px-2 pb-2">
<div className="mr-8 flex space-x-1">
<p className="font-semibold text-slate-700 underline-offset-4 group-hover:underline">
{summaryItem.title}
</p>
<div>
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
{convertFloatToNDecimal(summaryItem.percentage, 1)}%
</p>
</div>
</div>
<p className="flex w-32 items-end justify-end text-slate-600">
{summaryItem.count} {summaryItem.count === 1 ? "response" : "responses"}
</p>
</div>
</div>
<p className="flex w-32 items-end justify-end text-slate-600">
{questionSummary.accepted.count}{" "}
{questionSummary.accepted.count === 1 ? "response" : "responses"}
</p>
</div>
<ProgressBar barColor="bg-brand-dark" progress={questionSummary.accepted.percentage / 100} />
</div>
<div>
<div className="text flex justify-between px-2 pb-2">
<div className="mr-8 flex space-x-1">
<p className="font-semibold text-slate-700">Dismissed</p>
<div>
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
{convertFloatToNDecimal(questionSummary.dismissed.percentage, 1)}%
</p>
<div className="group-hover:opacity-80">
<ProgressBar barColor="bg-brand-dark" progress={summaryItem.percentage / 100} />
</div>
</div>
<p className="flex w-32 items-end justify-end text-slate-600">
{questionSummary.dismissed.count}{" "}
{questionSummary.dismissed.count === 1 ? "response" : "responses"}
</p>
</div>
<ProgressBar barColor="bg-brand-dark" progress={questionSummary.dismissed.percentage / 100} />
</div>
);
})}
</div>
</div>
);

View File

@@ -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 = ({
)}>
<div
style={{ backgroundColor: `rgba(0,196,184,${getOpacityLevel(percentage)})` }}
className="hover:outline-brand-dark m-1 flex h-full w-40 cursor-default items-center justify-center rounded p-4 text-sm text-slate-950 hover:outline">
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}
</div>
</TooltipRenderer>

View File

@@ -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 = ({
/>
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
{results.map((result, resultsIdx) => (
<div key={result.value}>
<div
key={result.value}
className="group cursor-pointer"
onClick={() =>
setFilter(
questionSummary.question.id,
questionSummary.question.headline,
questionSummary.question.type,
questionSummary.type === "multipleChoiceSingle" ? "Includes either" : "Includes all",
[result.value]
)
}>
<div className="text flex flex-col justify-between px-2 pb-2 sm:flex-row">
<div className="mr-8 flex w-full justify-between space-x-1 sm:justify-normal">
<p className="font-semibold text-slate-700">
<p className="font-semibold text-slate-700 underline-offset-4 group-hover:underline">
{results.length - resultsIdx} - {result.value}
</p>
<div>
@@ -71,7 +96,9 @@ export const MultipleChoiceSummary = ({
{result.count} {result.count === 1 ? "response" : "responses"}
</p>
</div>
<ProgressBar barColor="bg-brand-dark" progress={result.percentage / 100} />
<div className="group-hover:opacity-80">
<ProgressBar barColor="bg-brand-dark" progress={result.percentage / 100} />
</div>
{result.others && result.others.length > 0 && (
<div className="mt-4 rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-2 content-center rounded-t-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">

View File

@@ -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 (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader
@@ -19,46 +64,34 @@ export const NPSSummary = ({ questionSummary, survey, attributeClasses }: NPSSum
attributeClasses={attributeClasses}
/>
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
{["promoters", "passives", "detractors"].map((group) => (
<div key={group}>
<div className="mb-2 flex justify-between">
{["promoters", "passives", "detractors", "dismissed"].map((group) => (
<div className="cursor-pointer hover:opacity-80" key={group} onClick={() => applyFilter(group)}>
<div
className={`mb-2 flex justify-between ${group === "dismissed" ? "mb-2 border-t bg-white pt-4 text-sm md:text-base" : ""}`}>
<div className="mr-8 flex space-x-1">
<p className="font-semibold capitalize text-slate-700">{group}</p>
<p
className={`font-semibold capitalize text-slate-700 ${group === "dismissed" ? "" : "text-slate-700"}`}>
{group}
</p>
<div>
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
{convertFloatToNDecimal(questionSummary[group].percentage, 1)}%
{convertFloatToNDecimal(questionSummary[group]?.percentage, 1)}%
</p>
</div>
</div>
<p className="flex w-32 items-end justify-end text-slate-600">
{questionSummary[group].count} {questionSummary[group].count === 1 ? "response" : "responses"}
{questionSummary[group]?.count}{" "}
{questionSummary[group]?.count === 1 ? "response" : "responses"}
</p>
</div>
<ProgressBar barColor="bg-brand-dark" progress={questionSummary[group].percentage / 100} />
<ProgressBar
barColor={group === "dismissed" ? "bg-slate-600" : "bg-brand-dark"}
progress={questionSummary[group]?.percentage / 100}
/>
</div>
))}
</div>
{questionSummary.dismissed?.count > 0 && (
<div className="border-t bg-white px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
<div key={"dismissed"}>
<div className="text flex justify-between px-2 pb-2">
<div className="mr-8 flex space-x-1">
<p className="font-semibold text-slate-700">dismissed</p>
<div>
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
{convertFloatToNDecimal(questionSummary.dismissed.percentage, 1)}%
</p>
</div>
</div>
<p className="flex w-32 items-end justify-end text-slate-600">
{questionSummary.dismissed.count}{" "}
{questionSummary.dismissed.count === 1 ? "response" : "responses"}
</p>
</div>
<ProgressBar barColor="bg-slate-600" progress={questionSummary.dismissed.percentage / 100} />
</div>
</div>
)}
<div className="flex justify-center pb-4 pt-4">
<HalfCircle value={questionSummary.score} />
</div>

View File

@@ -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}
/>
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
{results.map((result) => (
<div key={result.id}>
{results.map((result, index) => (
<div
className="cursor-pointer hover:opacity-80"
key={result.id}
onClick={() =>
setFilter(
questionSummary.question.id,
questionSummary.question.headline,
questionSummary.question.type,
"Includes all",
[`Picture ${index + 1}`]
)
}>
<div className="text flex flex-col justify-between px-2 pb-2 sm:flex-row">
<div className="mr-8 flex w-full justify-between space-x-1 sm:justify-normal">
<div className="relative h-32 w-[220px]">

View File

@@ -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 <CircleSlash2 className="h-4 w-4" />;
@@ -36,7 +53,18 @@ export const RatingSummary = ({ questionSummary, survey, attributeClasses }: Rat
/>
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
{questionSummary.choices.map((result) => (
<div key={result.rating}>
<div
className="cursor-pointer hover:opacity-80"
key={result.rating}
onClick={() =>
setFilter(
questionSummary.question.id,
questionSummary.question.headline,
questionSummary.question.type,
"Is equal to",
result.rating.toString()
)
}>
<div className="text flex justify-between px-2 pb-2">
<div className="mr-8 flex items-center space-x-1">
<div className="font-semibold text-slate-700">

View File

@@ -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 (
<div className="mt-10 space-y-8">
{(survey.type === "app" || survey.type === "website") &&
responseCount === 0 &&
!widgetSetupCompleted ? (
<EmptyAppSurveys environment={environment} surveyType={survey.type} />
) : fetchingSummary ? (
) : summary.length === 0 ? (
<SkeletonLoader type="summary" />
) : responseCount === 0 ? (
<EmptySpaceFiller
@@ -83,6 +144,7 @@ export const SummaryList = ({
surveyType={survey.type}
survey={survey}
attributeClasses={attributeClasses}
setFilter={setFilter}
/>
);
}
@@ -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}
/>
);
}

View File

@@ -64,7 +64,6 @@ export const SummaryPage = ({
const [responseCount, setResponseCount] = useState<number | null>(null);
const [surveySummary, setSurveySummary] = useState<TSurveySummary>(initialSurveySummary);
const [showDropOffs, setShowDropOffs] = useState<boolean>(false);
const [isFetchingSummary, setFetchingSummary] = useState<boolean>(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}
/>

View File

@@ -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}`;
}
};

View File

@@ -120,7 +120,7 @@ export const QuestionFilterComboBox = ({
disabled || isDisabledComboBox || !filterValue ? "opacity-50" : "cursor-pointer"
)}>
{filterComboBoxValue && filterComboBoxValue?.length > 0 ? (
!isMultiple ? (
!Array.isArray(filterComboBoxValue) ? (
<p className="text-slate-600">{filterComboBoxValue}</p>
) : (
<div className="no-scrollbar flex w-[7rem] gap-3 overflow-auto md:w-[10rem] lg:w-[18rem]">
@@ -156,18 +156,14 @@ export const QuestionFilterComboBox = ({
{options?.map((o) => (
<CommandItem
onSelect={() => {
!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">

View File

@@ -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 (
<Popover open={isOpen} onOpenChange={handleOpenChange}>
<PopoverTrigger className="flex min-w-[8rem] items-center justify-between rounded border border-slate-200 bg-white p-3 text-sm text-slate-600 hover:border-slate-300 sm:min-w-[11rem] sm:px-6 sm:py-3">
Filter {filterValue.filter.length > 0 && `(${filterValue.filter.length})`}
<span>
Filter <b>{filterValue.filter.length > 0 && `(${filterValue.filter.length})`}</b>
</span>
<div className="ml-3">
{isOpen ? (
<ChevronUp className="ml-2 h-4 w-4 opacity-50" />
@@ -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]">
<div className="mb-8 flex flex-wrap items-start justify-between">
<p className="hidden text-lg font-bold text-black sm:block">Show all responses that match</p>
<p className="text-slate800 hidden text-lg font-semibold sm:block">Show all responses that match</p>
<p className="block text-base text-slate-500 sm:hidden">Show all responses where...</p>
<div className="flex items-center space-x-2">
<label className="text-sm font-normal text-slate-600">Only completed</label>

View File

@@ -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;
}
}
});

View File

@@ -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;
}
});

View File

@@ -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({