diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx index 8f120e2a12..5c66ea8af2 100755 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx @@ -5,11 +5,7 @@ import { useResponseFilter, } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext"; import { fetchFile } from "@/app/lib/fetchFile"; -import { - generateQuestionAndFilterOptions, - generateQuestionsAndAttributes, - getTodayDate, -} from "@/app/lib/surveys/surveys"; +import { generateQuestionAndFilterOptions, getTodayDate } from "@/app/lib/surveys/surveys"; import { createId } from "@paralleldrive/cuid2"; import { differenceInDays, format, subDays } from "date-fns"; import { ChevronDown, ChevronUp, DownloadIcon } from "lucide-react"; @@ -91,7 +87,7 @@ const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: Cu const datePickerRef = useRef(null); - const getMatchQandA = (responses: any, survey: any) => { + const getMatchQandA = (responses: TResponse[], survey: TSurvey) => { if (survey && responses) { // Create a mapping of question IDs to their headlines const questionIdToHeadline = {}; @@ -145,20 +141,58 @@ const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: Cu return "my_survey_responses"; }, [survey]); + function extracMetadataKeys(obj, parentKey = "") { + let keys: string[] = []; + + for (let key in obj) { + if (typeof obj[key] === "object" && obj[key] !== null) { + keys = keys.concat(extracMetadataKeys(obj[key], parentKey + key + " - ")); + } else { + keys.push(parentKey + key); + } + } + + return keys; + } + const downloadResponses = useCallback( async (filter: FilterDownload, filetype: "csv" | "xlsx") => { const downloadResponse = filter === FilterDownload.ALL ? totalResponses : responses; - const { attributeMap, questionNames } = generateQuestionsAndAttributes(survey, downloadResponse); + const questionNames = survey.questions?.map((question) => question.headline); + const hiddenFieldIds = survey.hiddenFields.fieldIds; + const hiddenFieldResponse = {}; + let metaDataFields = extracMetadataKeys(downloadResponse[0].meta); + const userAttributes = ["Init Attribute 1", "Init Attribute 2"]; const matchQandA = getMatchQandA(downloadResponse, survey); const jsonData = matchQandA.map((response) => { - const fileResponse = { + const basicInfo = { "Response ID": response.id, Timestamp: response.createdAt, Finished: response.finished, "Survey ID": response.surveyId, "Formbricks User ID": response.person?.id ?? "", }; + const metaDataKeys = extracMetadataKeys(response.meta); + let metaData = {}; + metaDataKeys.forEach((key) => { + if (!metaDataFields.includes(key)) metaDataFields.push(key); + if (response.meta) { + if (key.includes("-")) { + const nestedKeyArray = key.split("-"); + metaData[key] = response.meta[nestedKeyArray[0].trim()][nestedKeyArray[1].trim()] ?? ""; + } else { + metaData[key] = response.meta[key] ?? ""; + } + } + }); + const personAttributes = response.personAttributes; + if (hiddenFieldIds && hiddenFieldIds.length > 0) { + hiddenFieldIds.forEach((hiddenFieldId) => { + hiddenFieldResponse[hiddenFieldId] = response.data[hiddenFieldId] ?? ""; + }); + } + const fileResponse = { ...basicInfo, ...metaData, ...personAttributes, ...hiddenFieldResponse }; // Map each question name to its corresponding answer questionNames.forEach((questionName: string) => { const matchingQuestion = response.responses.find((question) => question.question === questionName); @@ -177,18 +211,6 @@ const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: Cu return fileResponse; }); - // Add attribute columns to the file - Object.keys(attributeMap).forEach((attributeName) => { - const attributeValues = attributeMap[attributeName]; - Object.keys(attributeValues).forEach((personId) => { - const value = attributeValues[personId]; - const matchingResponse = jsonData.find((response) => response["Formbricks User ID"] === personId); - if (matchingResponse) { - matchingResponse[attributeName] = value; - } - }); - }); - // Fields which will be used as column headers in the file const fields = [ "Response ID", @@ -196,8 +218,10 @@ const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: Cu "Finished", "Survey ID", "Formbricks User ID", - ...Object.keys(attributeMap), + ...metaDataFields, ...questionNames, + ...(hiddenFieldIds ?? []), + ...(survey.type === "web" ? userAttributes : []), ]; let response; diff --git a/apps/web/app/lib/surveys/surveys.ts b/apps/web/app/lib/surveys/surveys.ts index 874c600d85..c06fae8b80 100644 --- a/apps/web/app/lib/surveys/surveys.ts +++ b/apps/web/app/lib/surveys/surveys.ts @@ -14,35 +14,6 @@ import { TSurveyQuestionType } from "@formbricks/types/surveys"; import { TSurvey } from "@formbricks/types/surveys"; import { TTag } from "@formbricks/types/tags"; -export const generateQuestionsAndAttributes = (survey: TSurvey, responses: TResponse[]) => { - let questionNames: string[] = []; - - if (survey?.questions) { - questionNames = survey.questions.map((question) => question.headline); - } - - const attributeMap: Record> = {}; - - if (responses) { - responses.forEach((response) => { - const { person } = response; - if (person !== null) { - const { id, attributes } = person; - Object.keys(attributes).forEach((attributeName) => { - if (!attributeMap.hasOwnProperty(attributeName)) { - attributeMap[attributeName] = {}; - } - attributeMap[attributeName][id] = attributes[attributeName]; - }); - } - }); - } - return { - questionNames, - attributeMap, - }; -}; - const conditionOptions = { openText: ["is"], multipleChoiceSingle: ["Includes either"],