mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-11 17:19:33 -06:00
feat: Add response filtering for meta data (#2363)
Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
@@ -21,7 +21,7 @@ import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TMembershipRole } from "@formbricks/types/memberships";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TResponse, TSurveyPersonAttributes } from "@formbricks/types/responses";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
@@ -37,7 +37,6 @@ interface ResponsePageProps {
|
||||
product: TProduct;
|
||||
user?: TUser;
|
||||
environmentTags: TTag[];
|
||||
attributes: TSurveyPersonAttributes;
|
||||
responsesPerPage: number;
|
||||
membershipRole?: TMembershipRole;
|
||||
totalResponseCount: number;
|
||||
@@ -51,7 +50,6 @@ const ResponsePage = ({
|
||||
product,
|
||||
user,
|
||||
environmentTags,
|
||||
attributes,
|
||||
responsesPerPage,
|
||||
membershipRole,
|
||||
totalResponseCount,
|
||||
@@ -176,7 +174,7 @@ const ResponsePage = ({
|
||||
membershipRole={membershipRole}
|
||||
/>
|
||||
<div className="flex gap-1.5">
|
||||
<CustomFilter environmentTags={environmentTags} attributes={attributes} survey={survey} />
|
||||
<CustomFilter survey={survey} />
|
||||
{!isSharingPage && <ResultsShareButton survey={survey} webAppUrl={webAppUrl} user={user} />}
|
||||
</div>
|
||||
<SurveyResultsTabs
|
||||
|
||||
@@ -6,7 +6,7 @@ import { RESPONSES_PER_PAGE, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getResponseCountBySurveyId, getResponsePersonAttributes } from "@formbricks/lib/response/service";
|
||||
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
@@ -39,7 +39,6 @@ export default async function Page({ params }) {
|
||||
}
|
||||
const tags = await getTagsByEnvironmentId(params.environmentId);
|
||||
const team = await getTeamByEnvironmentId(params.environmentId);
|
||||
const attributes = await getResponsePersonAttributes(params.surveyId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
@@ -58,7 +57,6 @@ export default async function Page({ params }) {
|
||||
webAppUrl={WEBAPP_URL}
|
||||
product={product}
|
||||
environmentTags={tags}
|
||||
attributes={attributes}
|
||||
user={user}
|
||||
responsesPerPage={RESPONSES_PER_PAGE}
|
||||
membershipRole={currentUserMembership?.role}
|
||||
|
||||
@@ -23,9 +23,8 @@ import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TMembershipRole } from "@formbricks/types/memberships";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurveyPersonAttributes, TSurveySummary } from "@formbricks/types/responses";
|
||||
import { TSurveySummary } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import ContentWrapper from "@formbricks/ui/ContentWrapper";
|
||||
|
||||
@@ -53,8 +52,6 @@ interface SummaryPageProps {
|
||||
webAppUrl: string;
|
||||
product: TProduct;
|
||||
user?: TUser;
|
||||
environmentTags: TTag[];
|
||||
attributes: TSurveyPersonAttributes;
|
||||
membershipRole?: TMembershipRole;
|
||||
totalResponseCount: number;
|
||||
}
|
||||
@@ -66,8 +63,6 @@ const SummaryPage = ({
|
||||
product,
|
||||
webAppUrl,
|
||||
user,
|
||||
environmentTags,
|
||||
attributes,
|
||||
membershipRole,
|
||||
totalResponseCount,
|
||||
}: SummaryPageProps) => {
|
||||
@@ -142,7 +137,7 @@ const SummaryPage = ({
|
||||
membershipRole={membershipRole}
|
||||
/>
|
||||
<div className="flex gap-1.5">
|
||||
<CustomFilter environmentTags={environmentTags} attributes={attributes} survey={survey} />
|
||||
<CustomFilter survey={survey} />
|
||||
{!isSharingPage && <ResultsShareButton survey={survey} webAppUrl={webAppUrl} user={user} />}
|
||||
</div>
|
||||
<SurveyResultsTabs
|
||||
|
||||
@@ -7,9 +7,8 @@ import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getResponseCountBySurveyId, getResponsePersonAttributes } from "@formbricks/lib/response/service";
|
||||
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
|
||||
@@ -52,9 +51,6 @@ export default async function Page({ params }) {
|
||||
throw new Error("Team not found");
|
||||
}
|
||||
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
|
||||
|
||||
const tags = await getTagsByEnvironmentId(params.environmentId);
|
||||
const attributes = await getResponsePersonAttributes(params.surveyId);
|
||||
const totalResponseCount = await getResponseCountBySurveyId(params.surveyId);
|
||||
|
||||
return (
|
||||
@@ -66,8 +62,6 @@ export default async function Page({ params }) {
|
||||
webAppUrl={WEBAPP_URL}
|
||||
product={product}
|
||||
user={user}
|
||||
environmentTags={tags}
|
||||
attributes={attributes}
|
||||
membershipRole={currentUserMembership?.role}
|
||||
totalResponseCount={totalResponseCount}
|
||||
/>
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getResponseDownloadUrl } from "@formbricks/lib/response/service";
|
||||
import {
|
||||
getResponseDownloadUrl,
|
||||
getResponseMeta,
|
||||
getResponsePersonAttributes,
|
||||
} from "@formbricks/lib/response/service";
|
||||
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TResponseFilterCriteria } from "@formbricks/types/responses";
|
||||
|
||||
@@ -21,3 +26,19 @@ export async function getResponsesDownloadUrlAction(
|
||||
|
||||
return getResponseDownloadUrl(surveyId, format, filterCritera);
|
||||
}
|
||||
|
||||
export async function getSurveyFilterDataAction(surveyId: string, environmentId: string) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const [tags, attributes, meta] = await Promise.all([
|
||||
getTagsByEnvironmentId(environmentId),
|
||||
getResponsePersonAttributes(surveyId),
|
||||
getResponseMeta(surveyId),
|
||||
]);
|
||||
|
||||
return { environmentTags: tags, attributes, meta };
|
||||
}
|
||||
|
||||
@@ -5,11 +5,7 @@ import {
|
||||
useResponseFilter,
|
||||
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import { getResponsesDownloadUrlAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions";
|
||||
import {
|
||||
generateQuestionAndFilterOptions,
|
||||
getFormattedFilters,
|
||||
getTodayDate,
|
||||
} from "@/app/lib/surveys/surveys";
|
||||
import { getFormattedFilters, getTodayDate } from "@/app/lib/surveys/surveys";
|
||||
import { differenceInDays, format, startOfDay, subDays } from "date-fns";
|
||||
import { ChevronDown, ChevronUp, DownloadIcon } from "lucide-react";
|
||||
import { useParams } from "next/navigation";
|
||||
@@ -17,9 +13,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { useClickOutside } from "@formbricks/lib/utils/hooks/useClickOutside";
|
||||
import { TSurveyPersonAttributes } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { Calendar } from "@formbricks/ui/Calendar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -48,8 +42,6 @@ enum FilterDropDownLabels {
|
||||
}
|
||||
|
||||
interface CustomFilterProps {
|
||||
environmentTags: TTag[];
|
||||
attributes: TSurveyPersonAttributes;
|
||||
survey: TSurvey;
|
||||
}
|
||||
|
||||
@@ -64,11 +56,11 @@ const getDifferenceOfDays = (from, to) => {
|
||||
}
|
||||
};
|
||||
|
||||
const CustomFilter = ({ environmentTags, attributes, survey }: CustomFilterProps) => {
|
||||
const CustomFilter = ({ survey }: CustomFilterProps) => {
|
||||
const params = useParams();
|
||||
const isSharingPage = !!params.sharingKey;
|
||||
|
||||
const { selectedFilter, setSelectedOptions, dateRange, setDateRange, resetState } = useResponseFilter();
|
||||
const { selectedFilter, dateRange, setDateRange, resetState } = useResponseFilter();
|
||||
const [filterRange, setFilterRange] = useState<FilterDropDownLabels>(
|
||||
dateRange.from && dateRange.to
|
||||
? getDifferenceOfDays(dateRange.from, dateRange.to)
|
||||
@@ -95,16 +87,6 @@ const CustomFilter = ({ environmentTags, attributes, survey }: CustomFilterProps
|
||||
}
|
||||
}, [survey?.id, resetState]);
|
||||
|
||||
// when the page loads we get total responses and iterate over the responses and questions, tags and attributes to create the filter options
|
||||
useEffect(() => {
|
||||
const { questionFilterOptions, questionOptions } = generateQuestionAndFilterOptions(
|
||||
survey,
|
||||
environmentTags,
|
||||
attributes
|
||||
);
|
||||
setSelectedOptions({ questionFilterOptions, questionOptions });
|
||||
}, [survey, setSelectedOptions, environmentTags, attributes]);
|
||||
|
||||
const filters = useMemo(
|
||||
() => getFormattedFilters(survey, selectedFilter, dateRange),
|
||||
[survey, selectedFilter, dateRange]
|
||||
@@ -210,7 +192,7 @@ const CustomFilter = ({ environmentTags, attributes, survey }: CustomFilterProps
|
||||
<>
|
||||
<div className="relative mb-12 flex justify-between">
|
||||
<div className="flex justify-stretch gap-x-1.5">
|
||||
<ResponseFilter />
|
||||
<ResponseFilter survey={survey} />
|
||||
<DropdownMenu
|
||||
onOpenChange={(value) => {
|
||||
value && handleDatePickerClose();
|
||||
|
||||
@@ -23,7 +23,13 @@ type QuestionFilterComboBoxProps = {
|
||||
filterComboBoxValue: string | string[] | undefined;
|
||||
onChangeFilterValue: (o: string) => void;
|
||||
onChangeFilterComboBoxValue: (o: string | string[]) => void;
|
||||
type: OptionsType.METADATA | TSurveyQuestionType | OptionsType.ATTRIBUTES | OptionsType.TAGS | undefined;
|
||||
type:
|
||||
| OptionsType.OTHERS
|
||||
| TSurveyQuestionType
|
||||
| OptionsType.ATTRIBUTES
|
||||
| OptionsType.TAGS
|
||||
| OptionsType.META
|
||||
| undefined;
|
||||
handleRemoveMultiSelect: (value: string[]) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
@@ -2,19 +2,23 @@
|
||||
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
AirplayIcon,
|
||||
CheckIcon,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
GlobeIcon,
|
||||
GridIcon,
|
||||
HashIcon,
|
||||
HelpCircleIcon,
|
||||
ImageIcon,
|
||||
LanguagesIcon,
|
||||
ListIcon,
|
||||
MessageSquareTextIcon,
|
||||
MousePointerClickIcon,
|
||||
Rows3Icon,
|
||||
SmartphoneIcon,
|
||||
StarIcon,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
@@ -35,7 +39,8 @@ export enum OptionsType {
|
||||
QUESTIONS = "Questions",
|
||||
TAGS = "Tags",
|
||||
ATTRIBUTES = "Attributes",
|
||||
METADATA = "Metadata",
|
||||
OTHERS = "Other Filters",
|
||||
META = "Meta",
|
||||
}
|
||||
|
||||
export type QuestionOption = {
|
||||
@@ -60,16 +65,18 @@ const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOpti
|
||||
switch (type) {
|
||||
case OptionsType.QUESTIONS:
|
||||
switch (questionType) {
|
||||
case TSurveyQuestionType.Rating:
|
||||
return <StarIcon width={18} className="text-white" />;
|
||||
case TSurveyQuestionType.CTA:
|
||||
return <MousePointerClickIcon width={18} className="text-white" />;
|
||||
case TSurveyQuestionType.OpenText:
|
||||
return <HelpCircleIcon width={18} className="text-white" />;
|
||||
return <MessageSquareTextIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionType.Rating:
|
||||
return <StarIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionType.CTA:
|
||||
return <MousePointerClickIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionType.OpenText:
|
||||
return <HelpCircleIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionType.MultipleChoiceMulti:
|
||||
return <ListIcon width={18} className="text-white" />;
|
||||
return <ListIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionType.MultipleChoiceSingle:
|
||||
return <Rows3Icon width={18} className="text-white" />;
|
||||
return <Rows3Icon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionType.NPS:
|
||||
return <NetPromoterScoreIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionType.Consent:
|
||||
@@ -80,13 +87,24 @@ const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOpti
|
||||
return <GridIcon width={18} className="text-white" />;
|
||||
}
|
||||
case OptionsType.ATTRIBUTES:
|
||||
return <HashIcon width={18} className="text-white" />;
|
||||
case OptionsType.METADATA:
|
||||
return <User width={18} height={18} className="text-white" />;
|
||||
case OptionsType.META:
|
||||
switch (label) {
|
||||
case "device":
|
||||
return <SmartphoneIcon width={18} height={18} className="text-white" />;
|
||||
case "os":
|
||||
return <AirplayIcon width={18} height={18} className="text-white" />;
|
||||
case "browser":
|
||||
return <GlobeIcon width={18} height={18} className="text-white" />;
|
||||
case "source":
|
||||
return <GlobeIcon width={18} height={18} className="text-white" />;
|
||||
case "action":
|
||||
return <MousePointerClickIcon width={18} height={18} className="text-white" />;
|
||||
}
|
||||
case OptionsType.OTHERS:
|
||||
switch (label) {
|
||||
case "Language":
|
||||
return <LanguagesIcon width={18} height={18} className="text-white" />;
|
||||
case "Device Type":
|
||||
return <SmartphoneIcon width={18} height={18} className="text-white" />;
|
||||
}
|
||||
case OptionsType.TAGS:
|
||||
return <HashIcon width={18} className="text-white" />;
|
||||
|
||||
@@ -4,14 +4,18 @@ import {
|
||||
SelectedFilterValue,
|
||||
useResponseFilter,
|
||||
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import { getSurveyFilterDataAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions";
|
||||
import QuestionFilterComboBox from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox";
|
||||
import { generateQuestionAndFilterOptions } from "@/app/lib/surveys/surveys";
|
||||
import { getSurveyFilterDataBySurveySharingKeyAction } from "@/app/share/[sharingKey]/action";
|
||||
import clsx from "clsx";
|
||||
import { isEqual } from "lodash";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { ChevronDown, ChevronUp, Plus } from "lucide-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { TSurveyTSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import { TSurvey, TSurveyTSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Checkbox } from "@formbricks/ui/Checkbox";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui/Popover";
|
||||
@@ -25,11 +29,40 @@ export type QuestionFilterOptions = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const ResponseFilter = () => {
|
||||
interface ResponseFilterProps {
|
||||
survey: TSurvey;
|
||||
}
|
||||
|
||||
const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
const params = useParams();
|
||||
const sharingKey = params.sharingKey as string;
|
||||
const isSharingPage = !!sharingKey;
|
||||
|
||||
const { selectedFilter, setSelectedFilter, selectedOptions, setSelectedOptions } = useResponseFilter();
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const { selectedFilter, setSelectedFilter, selectedOptions } = useResponseFilter();
|
||||
const [filterValue, setFilterValue] = useState<SelectedFilterValue>(selectedFilter);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch the initial data for the filter and load it into the state
|
||||
const handleInitialData = async () => {
|
||||
if (isOpen) {
|
||||
const { attributes, meta, environmentTags } = isSharingPage
|
||||
? await getSurveyFilterDataBySurveySharingKeyAction(sharingKey, survey.environmentId)
|
||||
: await getSurveyFilterDataAction(survey.id, survey.environmentId);
|
||||
|
||||
const { questionFilterOptions, questionOptions } = generateQuestionAndFilterOptions(
|
||||
survey,
|
||||
environmentTags,
|
||||
attributes,
|
||||
meta
|
||||
);
|
||||
setSelectedOptions({ questionFilterOptions, questionOptions });
|
||||
}
|
||||
};
|
||||
|
||||
handleInitialData();
|
||||
}, [isOpen, isSharingPage, setSelectedOptions, sharingKey, survey]);
|
||||
|
||||
const handleOnChangeQuestionComboBoxValue = (value: QuestionOption, index: number) => {
|
||||
if (filterValue.filter[index].questionType) {
|
||||
// Create a new array and copy existing values from SelectedFilter
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
|
||||
import { QuestionFilterOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter";
|
||||
|
||||
import { TResponseFilterCriteria, TSurveyPersonAttributes } from "@formbricks/types/responses";
|
||||
import {
|
||||
TResponseFilterCriteria,
|
||||
TSurveyMetaFieldFilter,
|
||||
TSurveyPersonAttributes,
|
||||
} from "@formbricks/types/responses";
|
||||
import { TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
@@ -42,7 +46,8 @@ const filterOptions = {
|
||||
export const generateQuestionAndFilterOptions = (
|
||||
survey: TSurvey,
|
||||
environmentTags: TTag[] | undefined,
|
||||
attributes: TSurveyPersonAttributes
|
||||
attributes: TSurveyPersonAttributes,
|
||||
meta: TSurveyMetaFieldFilter
|
||||
): {
|
||||
questionOptions: QuestionOptions[];
|
||||
questionFilterOptions: QuestionFilterOptions[];
|
||||
@@ -137,19 +142,40 @@ export const generateQuestionAndFilterOptions = (
|
||||
});
|
||||
}
|
||||
|
||||
let metadataOptions: QuestionOption[] = [];
|
||||
if (meta) {
|
||||
questionOptions = [
|
||||
...questionOptions,
|
||||
{
|
||||
header: OptionsType.META,
|
||||
option: Object.keys(meta).map((m) => {
|
||||
return { label: m, type: OptionsType.META, id: m };
|
||||
}),
|
||||
},
|
||||
];
|
||||
Object.keys(meta).forEach((m) => {
|
||||
questionFilterOptions.push({
|
||||
type: "Meta",
|
||||
filterOptions: ["Equals", "Not equals"],
|
||||
filterComboBoxOptions: meta[m],
|
||||
id: m,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let languageQuestion: QuestionOption[] = [];
|
||||
|
||||
//can be extended to include more properties
|
||||
if (survey.languages?.length > 0) {
|
||||
metadataOptions.push({ label: "Language", type: OptionsType.METADATA, id: "language" });
|
||||
languageQuestion.push({ label: "Language", type: OptionsType.OTHERS, id: "language" });
|
||||
const languageOptions = survey.languages.map((sl) => sl.language.code);
|
||||
questionFilterOptions.push({
|
||||
type: "Metadata",
|
||||
type: OptionsType.OTHERS,
|
||||
filterOptions: conditionOptions.languages,
|
||||
filterComboBoxOptions: languageOptions,
|
||||
id: "language",
|
||||
});
|
||||
}
|
||||
questionOptions = [...questionOptions, { header: OptionsType.METADATA, option: metadataOptions }];
|
||||
questionOptions = [...questionOptions, { header: OptionsType.OTHERS, option: languageQuestion }];
|
||||
|
||||
return { questionOptions: [...questionOptions], questionFilterOptions: [...questionFilterOptions] };
|
||||
};
|
||||
@@ -161,20 +187,22 @@ export const getFormattedFilters = (
|
||||
dateRange: DateRange
|
||||
): TResponseFilterCriteria => {
|
||||
const filters: TResponseFilterCriteria = {};
|
||||
const [questions, tags, attributes, metadata] = selectedFilter.filter.reduce(
|
||||
(result: [FilterValue[], FilterValue[], FilterValue[], FilterValue[]], filter) => {
|
||||
const [questions, tags, attributes, others, meta] = selectedFilter.filter.reduce(
|
||||
(result: [FilterValue[], FilterValue[], FilterValue[], FilterValue[], FilterValue[]], filter) => {
|
||||
if (filter.questionType?.type === "Questions") {
|
||||
result[0].push(filter);
|
||||
} else if (filter.questionType?.type === "Tags") {
|
||||
result[1].push(filter);
|
||||
} else if (filter.questionType?.type === "Attributes") {
|
||||
result[2].push(filter);
|
||||
} else if (filter.questionType?.type === "Metadata") {
|
||||
} else if (filter.questionType?.type === "Other Filters") {
|
||||
result[3].push(filter);
|
||||
} else if (filter.questionType?.type === "Meta") {
|
||||
result[4].push(filter);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
[[], [], [], []]
|
||||
[[], [], [], [], []]
|
||||
);
|
||||
|
||||
// for completed responses
|
||||
@@ -328,6 +356,7 @@ export const getFormattedFilters = (
|
||||
});
|
||||
}
|
||||
|
||||
// for attributes
|
||||
if (attributes.length) {
|
||||
attributes.forEach(({ filterType, questionType }) => {
|
||||
if (!filters.personAttributes) filters.personAttributes = {};
|
||||
@@ -345,24 +374,42 @@ export const getFormattedFilters = (
|
||||
});
|
||||
}
|
||||
|
||||
// for metadata
|
||||
if (metadata.length) {
|
||||
metadata.forEach(({ filterType, questionType }) => {
|
||||
if (!filters.metadata) filters.metadata = {};
|
||||
|
||||
// for others
|
||||
if (others.length) {
|
||||
others.forEach(({ filterType, questionType }) => {
|
||||
if (!filters.others) filters.others = {};
|
||||
if (filterType.filterValue === "Equals") {
|
||||
filters.metadata[questionType.label ?? ""] = {
|
||||
filters.others[questionType.label ?? ""] = {
|
||||
op: "equals",
|
||||
value: filterType.filterComboBoxValue as string,
|
||||
};
|
||||
} else if (filterType.filterValue === "Not equals") {
|
||||
filters.metadata[questionType.label ?? ""] = {
|
||||
filters.others[questionType.label ?? ""] = {
|
||||
op: "notEquals",
|
||||
value: filterType.filterComboBoxValue as string,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// for meta
|
||||
if (meta.length) {
|
||||
meta.forEach(({ filterType, questionType }) => {
|
||||
if (!filters.meta) filters.meta = {};
|
||||
if (filterType.filterValue === "Equals") {
|
||||
filters.meta[questionType.label ?? ""] = {
|
||||
op: "equals",
|
||||
value: filterType.filterComboBoxValue as string,
|
||||
};
|
||||
} else if (filterType.filterValue === "Not equals") {
|
||||
filters.meta[questionType.label ?? ""] = {
|
||||
op: "notEquals",
|
||||
value: filterType.filterComboBoxValue as string,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return filters;
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { notFound } from "next/navigation";
|
||||
import { RESPONSES_PER_PAGE, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getResponseCountBySurveyId, getResponsePersonAttributes } from "@formbricks/lib/response/service";
|
||||
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||
import { getSurvey, getSurveyIdByResultShareKey } from "@formbricks/lib/survey/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
|
||||
@@ -32,7 +32,6 @@ export default async function Page({ params }) {
|
||||
}
|
||||
|
||||
const tags = await getTagsByEnvironmentId(environment.id);
|
||||
const attributes = await getResponsePersonAttributes(surveyId);
|
||||
const totalResponseCount = await getResponseCountBySurveyId(surveyId);
|
||||
|
||||
return (
|
||||
@@ -44,7 +43,6 @@ export default async function Page({ params }) {
|
||||
webAppUrl={WEBAPP_URL}
|
||||
product={product}
|
||||
environmentTags={tags}
|
||||
attributes={attributes}
|
||||
responsesPerPage={RESPONSES_PER_PAGE}
|
||||
totalResponseCount={totalResponseCount}
|
||||
/>
|
||||
|
||||
@@ -4,9 +4,8 @@ import { notFound } from "next/navigation";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getResponseCountBySurveyId, getResponsePersonAttributes } from "@formbricks/lib/response/service";
|
||||
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||
import { getSurvey, getSurveyIdByResultShareKey } from "@formbricks/lib/survey/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
|
||||
export default async function Page({ params }) {
|
||||
const surveyId = await getSurveyIdByResultShareKey(params.sharingKey);
|
||||
@@ -31,8 +30,6 @@ export default async function Page({ params }) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
const tags = await getTagsByEnvironmentId(environment.id);
|
||||
const attributes = await getResponsePersonAttributes(surveyId);
|
||||
const totalResponseCount = await getResponseCountBySurveyId(surveyId);
|
||||
|
||||
return (
|
||||
@@ -43,8 +40,6 @@ export default async function Page({ params }) {
|
||||
surveyId={survey.id}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
product={product}
|
||||
environmentTags={tags}
|
||||
attributes={attributes}
|
||||
totalResponseCount={totalResponseCount}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
"use server";
|
||||
|
||||
import { getResponseCountBySurveyId, getResponses, getSurveySummary } from "@formbricks/lib/response/service";
|
||||
import {
|
||||
getResponseCountBySurveyId,
|
||||
getResponseMeta,
|
||||
getResponsePersonAttributes,
|
||||
getResponses,
|
||||
getSurveySummary,
|
||||
} from "@formbricks/lib/response/service";
|
||||
import { getSurveyIdByResultShareKey } from "@formbricks/lib/survey/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TResponse, TResponseFilterCriteria, TSurveySummary } from "@formbricks/types/responses";
|
||||
|
||||
@@ -38,3 +45,19 @@ export const getResponseCountBySurveySharingKeyAction = async (
|
||||
|
||||
return await getResponseCountBySurveyId(surveyId, filterCriteria);
|
||||
};
|
||||
|
||||
export const getSurveyFilterDataBySurveySharingKeyAction = async (
|
||||
sharingKey: string,
|
||||
environmentId: string
|
||||
) => {
|
||||
const surveyId = await getSurveyIdByResultShareKey(sharingKey);
|
||||
if (!surveyId) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const [tags, attributes, meta] = await Promise.all([
|
||||
getTagsByEnvironmentId(environmentId),
|
||||
getResponsePersonAttributes(surveyId),
|
||||
getResponseMeta(surveyId),
|
||||
]);
|
||||
|
||||
return { environmentTags: tags, attributes, meta };
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
TResponseInput,
|
||||
TResponseLegacyInput,
|
||||
TResponseUpdateInput,
|
||||
TSurveyMetaFieldFilter,
|
||||
TSurveyPersonAttributes,
|
||||
TSurveySummary,
|
||||
ZResponse,
|
||||
@@ -457,6 +458,70 @@ export const getResponsePersonAttributes = async (surveyId: string): Promise<TSu
|
||||
return responses;
|
||||
};
|
||||
|
||||
export const getResponseMeta = async (surveyId: string): Promise<TSurveyMetaFieldFilter> => {
|
||||
const meta = await unstable_cache(
|
||||
async () => {
|
||||
validateInputs([surveyId, ZId]);
|
||||
|
||||
try {
|
||||
const responseMeta = await prisma.response.findMany({
|
||||
where: {
|
||||
surveyId: surveyId,
|
||||
},
|
||||
select: {
|
||||
meta: true,
|
||||
},
|
||||
});
|
||||
|
||||
const meta: { [key: string]: Set<string> } = {};
|
||||
|
||||
responseMeta.forEach((response) => {
|
||||
Object.entries(response.meta).forEach(([key, value]) => {
|
||||
// skip url
|
||||
if (key === "url") return;
|
||||
|
||||
// Handling nested objects (like userAgent)
|
||||
if (typeof value === "object" && value !== null) {
|
||||
Object.entries(value).forEach(([nestedKey, nestedValue]) => {
|
||||
if (typeof nestedValue === "string" && nestedValue) {
|
||||
if (!meta[nestedKey]) {
|
||||
meta[nestedKey] = new Set();
|
||||
}
|
||||
meta[nestedKey].add(nestedValue);
|
||||
}
|
||||
});
|
||||
} else if (typeof value === "string" && value) {
|
||||
if (!meta[key]) {
|
||||
meta[key] = new Set();
|
||||
}
|
||||
meta[key].add(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Convert Set to Array
|
||||
const result = Object.fromEntries(
|
||||
Object.entries(meta).map(([key, valueSet]) => [key, Array.from(valueSet)])
|
||||
);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getResponseMeta-${surveyId}`],
|
||||
{
|
||||
tags: [responseCache.tag.bySurveyId(surveyId)],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
}
|
||||
)();
|
||||
|
||||
return meta;
|
||||
};
|
||||
|
||||
export const getResponses = async (
|
||||
surveyId: string,
|
||||
page?: number,
|
||||
|
||||
@@ -38,6 +38,7 @@ export function calculateTtcTotal(ttc: TResponseTtc) {
|
||||
|
||||
export const buildWhereClause = (filterCriteria?: TResponseFilterCriteria) => {
|
||||
const whereClause: Record<string, any>[] = [];
|
||||
|
||||
// For finished
|
||||
if (filterCriteria?.finished !== undefined) {
|
||||
whereClause.push({
|
||||
@@ -128,19 +129,56 @@ export const buildWhereClause = (filterCriteria?: TResponseFilterCriteria) => {
|
||||
});
|
||||
}
|
||||
|
||||
// For Metadata
|
||||
if (filterCriteria?.metadata) {
|
||||
const metadata: Prisma.ResponseWhereInput[] = [];
|
||||
// for meta
|
||||
if (filterCriteria?.meta) {
|
||||
const meta: Prisma.ResponseWhereInput[] = [];
|
||||
|
||||
Object.entries(filterCriteria.meta).forEach(([key, val]) => {
|
||||
let updatedKey: string[] = [];
|
||||
if (["browser", "os", "device"].includes(key)) {
|
||||
updatedKey = ["userAgent", key];
|
||||
} else {
|
||||
updatedKey = [key];
|
||||
}
|
||||
|
||||
Object.entries(filterCriteria.metadata).forEach(([key, val]) => {
|
||||
switch (val.op) {
|
||||
case "equals":
|
||||
metadata.push({
|
||||
meta.push({
|
||||
meta: {
|
||||
path: updatedKey,
|
||||
equals: val.value,
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "notEquals":
|
||||
meta.push({
|
||||
meta: {
|
||||
path: updatedKey,
|
||||
not: val.value,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
whereClause.push({
|
||||
AND: meta,
|
||||
});
|
||||
}
|
||||
|
||||
// For Language
|
||||
if (filterCriteria?.others) {
|
||||
const others: Prisma.ResponseWhereInput[] = [];
|
||||
|
||||
Object.entries(filterCriteria.others).forEach(([key, val]) => {
|
||||
switch (val.op) {
|
||||
case "equals":
|
||||
others.push({
|
||||
[key.toLocaleLowerCase()]: val.value,
|
||||
});
|
||||
break;
|
||||
case "notEquals":
|
||||
metadata.push({
|
||||
others.push({
|
||||
[key.toLocaleLowerCase()]: {
|
||||
not: val.value,
|
||||
},
|
||||
@@ -149,7 +187,7 @@ export const buildWhereClause = (filterCriteria?: TResponseFilterCriteria) => {
|
||||
}
|
||||
});
|
||||
whereClause.push({
|
||||
AND: metadata,
|
||||
AND: others,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,10 @@ export const ZSurveyPersonAttributes = z.record(z.array(z.string()));
|
||||
|
||||
export type TSurveyPersonAttributes = z.infer<typeof ZSurveyPersonAttributes>;
|
||||
|
||||
export const ZSurveyMetaFieldFilter = z.record(z.array(z.string()));
|
||||
|
||||
export type TSurveyMetaFieldFilter = z.infer<typeof ZSurveyMetaFieldFilter>;
|
||||
|
||||
const ZResponseFilterCriteriaDataLessThan = z.object({
|
||||
op: z.literal(ZSurveyLogicCondition.Values.lessThan),
|
||||
value: z.number(),
|
||||
@@ -158,7 +162,16 @@ export const ZResponseFilterCriteria = z.object({
|
||||
})
|
||||
.optional(),
|
||||
|
||||
metadata: z
|
||||
others: z
|
||||
.record(
|
||||
z.object({
|
||||
op: z.enum(["equals", "notEquals"]),
|
||||
value: z.union([z.string(), z.number()]),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
|
||||
meta: z
|
||||
.record(
|
||||
z.object({
|
||||
op: z.enum(["equals", "notEquals"]),
|
||||
|
||||
@@ -61,7 +61,7 @@ const CommandList = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
className={cn("max-h-full overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
Reference in New Issue
Block a user