From 9d33aa034a4cf0a5a68b5318ac3be4ce7d974f06 Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Thu, 30 May 2024 16:30:09 +0530 Subject: [PATCH] feat: Filter Responses by hidden field values (#2662) --- .../surveys/[surveyId]/actions.ts | 13 +- .../components/QuestionFilterComboBox.tsx | 8 +- .../components/QuestionsComboBox.tsx | 5 + .../[surveyId]/components/ResponseFilter.tsx | 5 +- apps/web/app/lib/surveys/surveys.ts | 86 ++++++++++--- apps/web/app/share/[sharingKey]/actions.ts | 10 +- packages/lib/response/service.ts | 99 +++------------ .../lib/response/tests/__mocks__/data.mock.ts | 105 +--------------- packages/lib/response/tests/response.test.ts | 43 ------- packages/lib/response/utils.ts | 116 +++++++++++++++++- packages/types/responses.ts | 4 + 11 files changed, 223 insertions(+), 271 deletions(-) diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts index 49a2beab14..665b33a745 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts @@ -3,11 +3,7 @@ import { getServerSession } from "next-auth"; import { authOptions } from "@formbricks/lib/authOptions"; -import { - getResponseDownloadUrl, - getResponseMeta, - getResponsePersonAttributes, -} from "@formbricks/lib/response/service"; +import { getResponseDownloadUrl, getResponseFilteringValues } from "@formbricks/lib/response/service"; import { canUserAccessSurvey, verifyUserRoleAccess } from "@formbricks/lib/survey/auth"; import { updateSurvey } from "@formbricks/lib/survey/service"; import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service"; @@ -36,13 +32,12 @@ export const getSurveyFilterDataAction = async (surveyId: string, environmentId: const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId); if (!isAuthorized) throw new AuthorizationError("Not authorized"); - const [tags, attributes, meta] = await Promise.all([ + const [tags, { personAttributes: attributes, meta, hiddenFields }] = await Promise.all([ getTagsByEnvironmentId(environmentId), - getResponsePersonAttributes(surveyId), - getResponseMeta(surveyId), + getResponseFilteringValues(surveyId), ]); - return { environmentTags: tags, attributes, meta }; + return { environmentTags: tags, attributes, meta, hiddenFields }; }; export const updateSurveyAction = async (survey: TSurvey): Promise => { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.tsx index a8e49e8f32..26792998d7 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox.tsx @@ -23,13 +23,7 @@ type QuestionFilterComboBoxProps = { filterComboBoxValue: string | string[] | undefined; onChangeFilterValue: (o: string) => void; onChangeFilterComboBoxValue: (o: string | string[]) => void; - type: - | OptionsType.OTHERS - | TSurveyQuestionType - | OptionsType.ATTRIBUTES - | OptionsType.TAGS - | OptionsType.META - | undefined; + type?: TSurveyQuestionType | Omit; handleRemoveMultiSelect: (value: string[]) => void; disabled?: boolean; }; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.tsx index 2805bea335..b004fced89 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox.tsx @@ -6,6 +6,7 @@ import { CheckIcon, ChevronDown, ChevronUp, + EyeOff, GlobeIcon, GridIcon, HashIcon, @@ -41,6 +42,7 @@ export enum OptionsType { ATTRIBUTES = "Attributes", OTHERS = "Other Filters", META = "Meta", + HIDDEN_FIELDS = "Hidden Fields", } export type QuestionOption = { @@ -88,6 +90,9 @@ const SelectedCommandItem = ({ label, questionType, type }: Partial; + + case OptionsType.HIDDEN_FIELDS: + return ; case OptionsType.META: switch (label) { case "device": diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter.tsx index 3932958959..dd8c5b67b0 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter.tsx @@ -46,7 +46,7 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => { // Fetch the initial data for the filter and load it into the state const handleInitialData = async () => { if (isOpen) { - const { attributes, meta, environmentTags } = isSharingPage + const { attributes, meta, environmentTags, hiddenFields } = isSharingPage ? await getSurveyFilterDataBySurveySharingKeyAction(sharingKey, survey.environmentId) : await getSurveyFilterDataAction(survey.id, survey.environmentId); @@ -54,7 +54,8 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => { survey, environmentTags, attributes, - meta + meta, + hiddenFields ); setSelectedOptions({ questionFilterOptions, questionOptions }); } diff --git a/apps/web/app/lib/surveys/surveys.ts b/apps/web/app/lib/surveys/surveys.ts index a4cc51f7a1..d60fdbbde3 100644 --- a/apps/web/app/lib/surveys/surveys.ts +++ b/apps/web/app/lib/surveys/surveys.ts @@ -12,6 +12,7 @@ import { QuestionFilterOptions } from "@/app/(app)/environments/[environmentId]/ import { TResponseFilterCriteria, + TResponseHiddenFieldsFilter, TSurveyMetaFieldFilter, TSurveyPersonAttributes, } from "@formbricks/types/responses"; @@ -49,7 +50,8 @@ export const generateQuestionAndFilterOptions = ( survey: TSurvey, environmentTags: TTag[] | undefined, attributes: TSurveyPersonAttributes, - meta: TSurveyMetaFieldFilter + meta: TSurveyMetaFieldFilter, + hiddenFields: TResponseHiddenFieldsFilter ): { questionOptions: QuestionOptions[]; questionFilterOptions: QuestionFilterOptions[]; @@ -164,6 +166,26 @@ export const generateQuestionAndFilterOptions = ( }); } + if (hiddenFields) { + questionOptions = [ + ...questionOptions, + { + header: OptionsType.HIDDEN_FIELDS, + option: Object.keys(hiddenFields).map((hiddenField) => { + return { label: hiddenField, type: OptionsType.HIDDEN_FIELDS, id: hiddenField }; + }), + }, + ]; + Object.keys(hiddenFields).forEach((hiddenField) => { + questionFilterOptions.push({ + type: "Hidden Fields", + filterOptions: ["Equals", "Not equals"], + filterComboBoxOptions: hiddenFields[hiddenField], + id: hiddenField, + }); + }); + } + let languageQuestion: QuestionOption[] = []; //can be extended to include more properties @@ -189,23 +211,29 @@ export const getFormattedFilters = ( dateRange: DateRange ): TResponseFilterCriteria => { const filters: TResponseFilterCriteria = {}; - 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 === "Other Filters") { - result[3].push(filter); - } else if (filter.questionType?.type === "Meta") { - result[4].push(filter); - } - return result; - }, - [[], [], [], [], []] - ); + + const questions: FilterValue[] = []; + const tags: FilterValue[] = []; + const attributes: FilterValue[] = []; + const others: FilterValue[] = []; + const meta: FilterValue[] = []; + const hiddenFields: FilterValue[] = []; + + selectedFilter.filter.forEach((filter) => { + if (filter.questionType?.type === "Questions") { + questions.push(filter); + } else if (filter.questionType?.type === "Tags") { + tags.push(filter); + } else if (filter.questionType?.type === "Attributes") { + attributes.push(filter); + } else if (filter.questionType?.type === "Other Filters") { + others.push(filter); + } else if (filter.questionType?.type === "Meta") { + meta.push(filter); + } else if (filter.questionType?.type === "Hidden Fields") { + hiddenFields.push(filter); + } + }); // for completed responses if (selectedFilter.onlyComplete) { @@ -359,10 +387,30 @@ export const getFormattedFilters = ( }); } + // for hidden fields + if (hiddenFields.length) { + hiddenFields.forEach(({ filterType, questionType }) => { + if (!filters.data) filters.data = {}; + if (!filterType.filterComboBoxValue) return; + if (filterType.filterValue === "Equals") { + filters.data[questionType.label ?? ""] = { + op: "equals", + value: filterType.filterComboBoxValue as string, + }; + } else if (filterType.filterValue === "Not equals") { + filters.data[questionType.label ?? ""] = { + op: "notEquals", + value: filterType.filterComboBoxValue as string, + }; + } + }); + } + // for attributes if (attributes.length) { attributes.forEach(({ filterType, questionType }) => { if (!filters.personAttributes) filters.personAttributes = {}; + if (!filterType.filterComboBoxValue) return; if (filterType.filterValue === "Equals") { filters.personAttributes[questionType.label ?? ""] = { op: "equals", @@ -381,6 +429,7 @@ export const getFormattedFilters = ( if (others.length) { others.forEach(({ filterType, questionType }) => { if (!filters.others) filters.others = {}; + if (!filterType.filterComboBoxValue) return; if (filterType.filterValue === "Equals") { filters.others[questionType.label ?? ""] = { op: "equals", @@ -399,6 +448,7 @@ export const getFormattedFilters = ( if (meta.length) { meta.forEach(({ filterType, questionType }) => { if (!filters.meta) filters.meta = {}; + if (!filterType.filterComboBoxValue) return; if (filterType.filterValue === "Equals") { filters.meta[questionType.label ?? ""] = { op: "equals", diff --git a/apps/web/app/share/[sharingKey]/actions.ts b/apps/web/app/share/[sharingKey]/actions.ts index 3e7e6f28f5..d867774199 100644 --- a/apps/web/app/share/[sharingKey]/actions.ts +++ b/apps/web/app/share/[sharingKey]/actions.ts @@ -2,8 +2,7 @@ import { getResponseCountBySurveyId, - getResponseMeta, - getResponsePersonAttributes, + getResponseFilteringValues, getResponses, getSurveySummary, } from "@formbricks/lib/response/service"; @@ -53,11 +52,10 @@ export const getSurveyFilterDataBySurveySharingKeyAction = async ( const surveyId = await getSurveyIdByResultShareKey(sharingKey); if (!surveyId) throw new AuthorizationError("Not authorized"); - const [tags, attributes, meta] = await Promise.all([ + const [tags, { personAttributes: attributes, meta, hiddenFields }] = await Promise.all([ getTagsByEnvironmentId(environmentId), - getResponsePersonAttributes(surveyId), - getResponseMeta(surveyId), + getResponseFilteringValues(surveyId), ]); - return { environmentTags: tags, attributes, meta }; + return { environmentTags: tags, attributes, meta, hiddenFields }; }; diff --git a/packages/lib/response/service.ts b/packages/lib/response/service.ts index 3c5205cf96..6086f9264f 100644 --- a/packages/lib/response/service.ts +++ b/packages/lib/response/service.ts @@ -14,8 +14,6 @@ import { TResponseInput, TResponseLegacyInput, TResponseUpdateInput, - TSurveyMetaFieldFilter, - TSurveyPersonAttributes, ZResponseFilterCriteria, ZResponseInput, ZResponseLegacyInput, @@ -43,6 +41,9 @@ import { calculateTtcTotal, extractSurveyDetails, getQuestionWiseSummary, + getResponseHiddenFields, + getResponseMeta, + getResponsePersonAttributes, getResponsesFileName, getResponsesJson, getSurveySummaryDropOff, @@ -387,37 +388,33 @@ export const getResponse = (responseId: string): Promise => } )(); -export const getResponsePersonAttributes = (surveyId: string): Promise => +export const getResponseFilteringValues = async (surveyId: string) => cache( async () => { validateInputs([surveyId, ZId]); try { - let attributes: TSurveyPersonAttributes = {}; - const responseAttributes = await prisma.response.findMany({ + const survey = await getSurvey(surveyId); + if (!survey) { + throw new ResourceNotFoundError("Survey", surveyId); + } + + const responses = await prisma.response.findMany({ where: { - surveyId: surveyId, + surveyId, }, select: { + data: true, + meta: true, personAttributes: true, }, }); - responseAttributes.forEach((response) => { - Object.keys(response.personAttributes ?? {}).forEach((key) => { - if (response.personAttributes && attributes[key]) { - attributes[key].push(response.personAttributes[key].toString()); - } else if (response.personAttributes) { - attributes[key] = [response.personAttributes[key].toString()]; - } - }); - }); + const personAttributes = getResponsePersonAttributes(responses); + const meta = getResponseMeta(responses); + const hiddenFields = getResponseHiddenFields(survey, responses); - Object.keys(attributes).forEach((key) => { - attributes[key] = Array.from(new Set(attributes[key])); - }); - - return attributes; + return { personAttributes, meta, hiddenFields }; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { throw new DatabaseError(error.message); @@ -426,67 +423,7 @@ export const getResponsePersonAttributes = (surveyId: string): Promise => - cache( - async () => { - validateInputs([surveyId, ZId]); - - try { - const responseMeta = await prisma.response.findMany({ - where: { - surveyId: surveyId, - }, - select: { - meta: true, - }, - }); - - const meta: { [key: string]: Set } = {}; - - 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}`], + [`getResponseFilteringValues-${surveyId}`], { tags: [responseCache.tag.bySurveyId(surveyId)], } diff --git a/packages/lib/response/tests/__mocks__/data.mock.ts b/packages/lib/response/tests/__mocks__/data.mock.ts index f836e968bd..fce5f7ec87 100644 --- a/packages/lib/response/tests/__mocks__/data.mock.ts +++ b/packages/lib/response/tests/__mocks__/data.mock.ts @@ -2,12 +2,7 @@ import { Prisma } from "@prisma/client"; import { isAfter, isBefore, isSameDay } from "date-fns"; import { TDisplay } from "@formbricks/types/displays"; -import { - TResponse, - TResponseFilterCriteria, - TResponseUpdateInput, - TSurveyPersonAttributes, -} from "@formbricks/types/responses"; +import { TResponse, TResponseFilterCriteria, TResponseUpdateInput } from "@formbricks/types/responses"; import { TSurveyQuestionType } from "@formbricks/types/surveys"; import { TTag } from "@formbricks/types/tags"; @@ -104,98 +99,6 @@ export const mockResponse: ResponseMock = { ttc: {}, }; -export const mockResponsePersonAttributes: ResponseMock[] = [ - { - id: mockResponseId, - surveyId: mockSurveyId, - singleUseId: mockSingleUseId, - data: {}, - createdAt: new Date(), - finished: constantsForTests.boolean, - meta: mockMeta, - notes: [mockResponseNote], - tags: mockTags, - personId: mockPersonId, - updatedAt: new Date(), - ttc: {}, - person: null, - language: null, - personAttributes: { Plan: "Paid", "Init Attribute 1": "one", "Init Attribute 2": "two" }, - }, - { - id: mockResponseId, - surveyId: mockSurveyId, - singleUseId: mockSingleUseId, - data: {}, - createdAt: new Date(), - finished: constantsForTests.boolean, - meta: mockMeta, - notes: [mockResponseNote], - tags: mockTags, - personId: mockPersonId, - updatedAt: new Date(), - ttc: {}, - person: null, - language: null, - personAttributes: { - Plan: "Paid", - "Init Attribute 1": "three", - "Init Attribute 2": "four", - }, - }, - { - id: mockResponseId, - surveyId: mockSurveyId, - singleUseId: mockSingleUseId, - data: {}, - createdAt: new Date(), - finished: constantsForTests.boolean, - meta: mockMeta, - notes: [mockResponseNote], - tags: mockTags, - personId: mockPersonId, - updatedAt: new Date(), - ttc: {}, - person: null, - language: null, - personAttributes: { Plan: "Paid", "Init Attribute 1": "five", "Init Attribute 2": "six" }, - }, - { - id: mockResponseId, - surveyId: mockSurveyId, - singleUseId: mockSingleUseId, - data: {}, - createdAt: new Date(), - finished: constantsForTests.boolean, - meta: mockMeta, - notes: [mockResponseNote], - tags: mockTags, - personId: mockPersonId, - updatedAt: new Date(), - ttc: {}, - person: null, - language: null, - personAttributes: { Plan: "Paid", "Init Attribute 1": "five", "Init Attribute 2": "four" }, - }, - { - id: mockResponseId, - surveyId: mockSurveyId, - singleUseId: mockSingleUseId, - data: {}, - createdAt: new Date(), - finished: constantsForTests.boolean, - meta: mockMeta, - notes: [mockResponseNote], - tags: mockTags, - personId: mockPersonId, - updatedAt: new Date(), - ttc: {}, - person: null, - language: null, - personAttributes: { Plan: "Paid", "Init Attribute 1": "three", "Init Attribute 2": "two" }, - }, -]; - const getMockTags = (tags: string[]): { tag: TTag }[] => { return tags.map((tag) => ({ tag: { @@ -445,12 +348,6 @@ export const mockResponseData: TResponseUpdateInput["data"] = { key3: 20, }; -export const mockPersonAttributesData: TSurveyPersonAttributes = { - Plan: ["Paid"], - "Init Attribute 1": ["one", "three", "five"], - "Init Attribute 2": ["two", "four", "six"], -}; - export const getMockUpdateResponseInput = (finished: boolean = false): TResponseUpdateInput => ({ data: mockResponseData, finished, diff --git a/packages/lib/response/tests/response.test.ts b/packages/lib/response/tests/response.test.ts index 66b132e226..ad550706b7 100644 --- a/packages/lib/response/tests/response.test.ts +++ b/packages/lib/response/tests/response.test.ts @@ -6,12 +6,10 @@ import { mockEnvironmentId, mockMeta, mockPerson, - mockPersonAttributesData, mockPersonId, mockResponse, mockResponseData, mockResponseNote, - mockResponsePersonAttributes, mockResponseWithMockPerson, mockSingleUseId, mockSurveyId, @@ -43,7 +41,6 @@ import { getResponseBySingleUseId, getResponseCountBySurveyId, getResponseDownloadUrl, - getResponsePersonAttributes, getResponses, getResponsesByEnvironmentId, getResponsesByPersonId, @@ -317,46 +314,6 @@ describe("Tests for getResponse service", () => { }); }); -describe("Tests for getAttributesFromResponses service", () => { - describe("Happy Path", () => { - it("Retrieves all attributes from responses for a given survey ID", async () => { - prisma.response.findMany.mockResolvedValue(mockResponsePersonAttributes); - const attributes = await getResponsePersonAttributes(mockSurveyId); - expect(attributes).toEqual(mockPersonAttributesData); - }); - - it("Returns an empty Object when no responses with attributes are found for the given survey ID", async () => { - prisma.response.findMany.mockResolvedValue([]); - - const responses = await getResponsePersonAttributes(mockSurveyId); - expect(responses).toEqual({}); - }); - }); - - describe("Sad Path", () => { - testInputValidation(getResponsePersonAttributes, "123#"); - - it("Throws DatabaseError on PrismaClientKnownRequestError", async () => { - const mockErrorMessage = "Mock error message"; - const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, { - code: "P2002", - clientVersion: "0.0.1", - }); - - prisma.response.findMany.mockRejectedValue(errToThrow); - - await expect(getResponsePersonAttributes(mockSurveyId)).rejects.toThrow(DatabaseError); - }); - - it("Throws a generic Error for unexpected problems", async () => { - const mockErrorMessage = "Mock error message"; - prisma.response.findMany.mockRejectedValue(new Error(mockErrorMessage)); - - await expect(getResponsePersonAttributes(mockSurveyId)).rejects.toThrow(Error); - }); - }); -}); - describe("Tests for getResponses service", () => { describe("Happy Path", () => { it("Fetches first 10 responses for a given survey ID", async () => { diff --git a/packages/lib/response/utils.ts b/packages/lib/response/utils.ts index 235aff76ca..50424d3b9b 100644 --- a/packages/lib/response/utils.ts +++ b/packages/lib/response/utils.ts @@ -2,7 +2,14 @@ import "server-only"; import { Prisma } from "@prisma/client"; -import { TResponse, TResponseFilterCriteria, TResponseTtc } from "@formbricks/types/responses"; +import { + TResponse, + TResponseFilterCriteria, + TResponseHiddenFieldsFilter, + TResponseTtc, + TSurveyMetaFieldFilter, + TSurveyPersonAttributes, +} from "@formbricks/types/responses"; import { TSurvey, TSurveyLanguage, @@ -1210,3 +1217,110 @@ export const getQuestionWiseSummary = ( return summary; }; + +export const getResponsePersonAttributes = ( + responses: Pick[] +): TSurveyPersonAttributes => { + try { + let attributes: TSurveyPersonAttributes = {}; + + responses.forEach((response) => { + Object.keys(response.personAttributes ?? {}).forEach((key) => { + if (response.personAttributes && attributes[key]) { + attributes[key].push(response.personAttributes[key].toString()); + } else if (response.personAttributes) { + attributes[key] = [response.personAttributes[key].toString()]; + } + }); + }); + + Object.keys(attributes).forEach((key) => { + attributes[key] = Array.from(new Set(attributes[key])); + }); + + return attributes; + } catch (error) { + throw error; + } +}; + +export const getResponseMeta = ( + responses: Pick[] +): TSurveyMetaFieldFilter => { + try { + const meta: { [key: string]: Set } = {}; + + responses.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) { + throw error; + } +}; + +export const getResponseHiddenFields = ( + survey: TSurvey, + responses: Pick[] +): TResponseHiddenFieldsFilter => { + try { + const hiddenFields: { [key: string]: Set } = {}; + + const surveyHiddenFields = survey?.hiddenFields.fieldIds; + const hasHiddenFields = surveyHiddenFields && surveyHiddenFields.length > 0; + + if (hasHiddenFields) { + // adding hidden fields to meta + survey?.hiddenFields.fieldIds?.forEach((fieldId) => { + hiddenFields[fieldId] = new Set(); + }); + + responses.forEach((response) => { + // Handling data fields(Hidden fields) + surveyHiddenFields?.forEach((fieldId) => { + const hiddenFieldValue = response.data[fieldId]; + if (hiddenFieldValue) { + if (typeof hiddenFieldValue === "string") { + hiddenFields[fieldId].add(hiddenFieldValue); + } + } + }); + }); + } + + // Convert Set to Array + const result = Object.fromEntries( + Object.entries(hiddenFields).map(([key, valueSet]) => [key, Array.from(valueSet)]) + ); + + return result; + } catch (error) { + throw error; + } +}; diff --git a/packages/types/responses.ts b/packages/types/responses.ts index e7616a7e19..71fdbd4b89 100644 --- a/packages/types/responses.ts +++ b/packages/types/responses.ts @@ -34,6 +34,10 @@ export const ZSurveyMetaFieldFilter = z.record(z.array(z.string())); export type TSurveyMetaFieldFilter = z.infer; +export const ZResponseHiddenFieldsFilter = z.record(z.array(z.string())); + +export type TResponseHiddenFieldsFilter = z.infer; + const ZResponseFilterCriteriaDataLessThan = z.object({ op: z.literal(ZSurveyLogicCondition.Values.lessThan), value: z.number(),