mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-22 00:52:50 -06:00
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:
committed by
GitHub
parent
8df722ab02
commit
3c3798ee98
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user