From a71ad7c15edc646a3ebfd2f60775e70936ee6d2a Mon Sep 17 00:00:00 2001 From: Ronit Panda <72537293+rtpa25@users.noreply.github.com> Date: Sun, 15 Oct 2023 22:54:12 +0530 Subject: [PATCH] feat: adds hidden field functionality to surveys (#1144) Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com> Co-authored-by: Matthias Nannt --- .../components/HiddenFieldsSummary.tsx | 104 ++++++++++ .../summary/components/SummaryList.tsx | 17 +- .../edit/components/HiddenFieldsCard.tsx | 178 ++++++++++++++++++ .../edit/components/QuestionsView.tsx | 18 +- .../edit/components/UpdateQuestionId.tsx | 25 ++- .../surveys/templates/templates.ts | 48 ++++- .../s/[surveyId]/components/LinkSurvey.tsx | 26 ++- packages/database/jsonTypes.ts | 4 +- .../migration.sql | 2 + packages/database/schema.prisma | 3 + packages/database/zod-utils.ts | 1 + packages/lib/survey/service.ts | 13 +- packages/types/v1/surveys.ts | 9 + packages/types/v1/templates.ts | 3 +- packages/ui/SingleResponseCard/index.tsx | 12 ++ packages/ui/Tag/index.tsx | 22 +-- 16 files changed, 448 insertions(+), 37 deletions(-) create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary.tsx create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/HiddenFieldsCard.tsx create mode 100644 packages/database/migrations/20231015165646_add_hidden_fields/migration.sql diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary.tsx new file mode 100644 index 0000000000..6f76a361f8 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary.tsx @@ -0,0 +1,104 @@ +import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/Headline"; +import { getPersonIdentifier } from "@formbricks/lib/people/helpers"; +import { timeSince } from "@formbricks/lib/time"; +import { TEnvironment } from "@formbricks/types/v1/environment"; +import { TResponse } from "@formbricks/types/v1/responses"; +import { TSurvey } from "@formbricks/types/v1/surveys"; +import { PersonAvatar } from "@formbricks/ui/Avatars"; +import { ChatBubbleBottomCenterTextIcon, InboxStackIcon } from "@heroicons/react/24/solid"; +import { Link } from "lucide-react"; +import { FC, useMemo } from "react"; + +interface HiddenFieldsSummaryProps { + question: string; + survey: TSurvey; + responses: TResponse[]; + environment: TEnvironment; +} + +const HiddenFieldsSummary: FC = ({ environment, responses, survey, question }) => { + const hiddenFieldResponses = useMemo( + () => + survey.hiddenFields?.fieldIds?.map((question) => { + const questionResponses = responses + .filter((response) => question in response.data) + .map((r) => ({ + id: r.id, + value: r.data[question], + updatedAt: r.updatedAt, + person: r.person, + })); + return { + question, + responses: questionResponses, + }; + }), + [responses, survey.hiddenFields?.fieldIds] + ); + + return ( +
+
+ + +
+
+ + Hidden Field +
+
+ + {hiddenFieldResponses?.find((q) => q.question === question)?.responses?.length} Responses +
+
+
+
+
+
User
+
Response
+
Time
+
+ {hiddenFieldResponses + ?.find((q) => q.question === question) + ?.responses.map((response) => { + const displayIdentifier = getPersonIdentifier(response.person!); + return ( +
+
+ {response.person ? ( + +
+ +
+

+ {displayIdentifier} +

+ + ) : ( +
+
+ +
+

Anonymous

+
+ )} +
+
+ {response.value} +
+
+ {timeSince(response.updatedAt.toISOString())} +
+
+ ); + })} +
+
+ ); +}; + +export default HiddenFieldsSummary; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx index a76f5ce157..4c612b0d08 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx @@ -1,7 +1,10 @@ +import EmptyInAppSurveys from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys"; import ConsentSummary from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary"; +import HiddenFieldsSummary from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary"; import EmptySpaceFiller from "@/app/components/shared/EmptySpaceFiller"; import { QuestionType } from "@formbricks/types/questions"; import type { QuestionSummary } from "@formbricks/types/responses"; +import { TEnvironment } from "@formbricks/types/v1/environment"; import { TResponse } from "@formbricks/types/v1/responses"; import { TSurvey, @@ -19,8 +22,6 @@ import MultipleChoiceSummary from "./MultipleChoiceSummary"; import NPSSummary from "./NPSSummary"; import OpenTextSummary from "./OpenTextSummary"; import RatingSummary from "./RatingSummary"; -import EmptyInAppSurveys from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys"; -import { TEnvironment } from "@formbricks/types/v1/environment"; interface SummaryListProps { environment: TEnvironment; @@ -119,6 +120,18 @@ export default function SummaryList({ environment, survey, responses }: SummaryL } return null; })} + {survey.hiddenFields?.enabled && + survey.hiddenFields.fieldIds?.map((question) => { + return ( + + ); + })} )} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/HiddenFieldsCard.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/HiddenFieldsCard.tsx new file mode 100644 index 0000000000..8f8fd1d1d6 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/HiddenFieldsCard.tsx @@ -0,0 +1,178 @@ +"use client"; + +import { cn } from "@formbricks/lib/cn"; +import { TSurveyHiddenFields, TSurveyQuestions, TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; +import { Input } from "@formbricks/ui/Input"; +import { Label } from "@formbricks/ui/Label"; +import { Switch } from "@formbricks/ui/Switch"; +import { Tag } from "@formbricks/ui/Tag"; +import * as Collapsible from "@radix-ui/react-collapsible"; +import { FC, useState } from "react"; +import toast from "react-hot-toast"; + +interface HiddenFieldsCardProps { + localSurvey: TSurveyWithAnalytics; + setLocalSurvey: (survey: TSurveyWithAnalytics) => void; + activeQuestionId: string | null; + setActiveQuestionId: (questionId: string | null) => void; +} + +const HiddenFieldsCard: FC = ({ + activeQuestionId, + localSurvey, + setActiveQuestionId, + setLocalSurvey, +}) => { + const open = activeQuestionId == "hidden"; + const [hiddenField, setHiddenField] = useState(""); + + const setOpen = (open: boolean) => { + if (open) { + setActiveQuestionId("hidden"); + } else { + setActiveQuestionId(null); + } + }; + + const updateSurvey = (data: TSurveyHiddenFields) => { + setLocalSurvey({ + ...localSurvey, + hiddenFields: { + ...localSurvey.hiddenFields, + ...data, + }, + }); + }; + + return ( +
+
+

👁️

+
+ + +
+
+
+

Hidden Fields

+
+
+ +
+ + + { + e.stopPropagation(); + updateSurvey({ enabled: !localSurvey.hiddenFields?.enabled }); + }} + /> +
+
+
+ +
+ {localSurvey.hiddenFields?.fieldIds && localSurvey.hiddenFields?.fieldIds?.length > 0 ? ( + localSurvey.hiddenFields?.fieldIds?.map((question) => { + return ( + { + updateSurvey({ + enabled: true, + fieldIds: localSurvey.hiddenFields?.fieldIds?.filter((q) => q !== question), + }); + }} + tagId={question} + tagName={question} + /> + ); + }) + ) : ( +

No hidden fields yet. Add the first one below.

+ )} +
+
{ + e.preventDefault(); + + const errorMessage = validateHiddenField( + // current field + hiddenField, + // existing fields + localSurvey.hiddenFields?.fieldIds || [], + // existing questions + localSurvey.questions + ); + + if (errorMessage !== "") return toast.error(errorMessage); + + updateSurvey({ + fieldIds: [...(localSurvey.hiddenFields?.fieldIds || []), hiddenField], + enabled: true, + }); + setHiddenField(""); + }}> + +
+ setHiddenField(e.target.value.trim())} + placeholder="Type field id..." + /> +
+
+
+
+
+ ); +}; + +export default HiddenFieldsCard; + +const validateHiddenField = ( + field: string, + existingFields: string[], + existingQuestions: TSurveyQuestions +): string => { + if (field.trim() === "") { + return "Please enter a question"; + } + // no duplicate questions + if (existingFields.findIndex((q) => q.toLowerCase() === field.toLowerCase()) !== -1) { + return "Question already exists"; + } + // no key words -- userId & suid & existing question ids + if (["userId", "suid"].includes(field) || existingQuestions.findIndex((q) => q.id === field) !== -1) { + return "Question not allowed"; + } + // no spaced words --> should be valid query param on url + if (field.includes(" ")) { + return "Question not allowed, avoid using spaces"; + } + // Check if the parameter contains only alphanumeric characters + if (!/^[a-zA-Z0-9]+$/.test(field)) { + return "Question not allowed, avoid using special characters"; + } + + return ""; +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx index 092f6b3bda..c032e70701 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx @@ -1,6 +1,8 @@ "use client"; -import React from "react"; +import HiddenFieldsCard from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/HiddenFieldsCard"; +import { TProduct } from "@formbricks/types/v1/product"; +import { TSurveyQuestion, TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; import { createId } from "@paralleldrive/cuid2"; import { useMemo, useState } from "react"; import { DragDropContext } from "react-beautiful-dnd"; @@ -9,10 +11,7 @@ import AddQuestionButton from "./AddQuestionButton"; import EditThankYouCard from "./EditThankYouCard"; import QuestionCard from "./QuestionCard"; import { StrictModeDroppable } from "./StrictModeDroppable"; -import { TSurveyQuestion } from "@formbricks/types/v1/surveys"; import { validateQuestion } from "./Validation"; -import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; -import { TProduct } from "@formbricks/types/v1/product"; interface QuestionsViewProps { localSurvey: TSurveyWithAnalytics; @@ -214,13 +213,22 @@ export default function QuestionsView({ -
+
+ + {localSurvey.type === "link" ? ( + + ) : null}
); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/UpdateQuestionId.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/UpdateQuestionId.tsx index d9769a1d00..90e5687934 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/UpdateQuestionId.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/UpdateQuestionId.tsx @@ -1,11 +1,24 @@ "use client"; +import { TSurveyWithAnalytics, TSurveyQuestion } from "@formbricks/types/v1/surveys"; import { Input } from "@formbricks/ui/Input"; import { Label } from "@formbricks/ui/Label"; import { useState } from "react"; import toast from "react-hot-toast"; -export default function UpdateQuestionId({ localSurvey, question, questionIdx, updateQuestion }) { +interface UpdateQuestionIdProps { + localSurvey: TSurveyWithAnalytics; + question: TSurveyQuestion; + questionIdx: number; + updateQuestion: (questionIdx: number, updatedAttributes: any) => void; +} + +export default function UpdateQuestionId({ + localSurvey, + question, + questionIdx, + updateQuestion, +}: UpdateQuestionIdProps) { const [currentValue, setCurrentValue] = useState(question.id); const [prevValue, setPrevValue] = useState(question.id); const [isInputInvalid, setIsInputInvalid] = useState( @@ -44,7 +57,15 @@ export default function UpdateQuestionId({ localSurvey, question, questionIdx, u id="questionId" name="questionId" value={currentValue} - onChange={(e) => setCurrentValue(e.target.value)} + onChange={(e) => { + setCurrentValue(e.target.value); + localSurvey.hiddenFields?.fieldIds?.forEach((field) => { + if (field === e.target.value) { + setIsInputInvalid(true); + toast.error("QuestionID can't be equal to hidden fields"); + } + }); + }} onBlur={saveAction} disabled={!(localSurvey.status === "draft" || question.isDraft)} className={isInputInvalid ? "border-red-300 focus:border-red-300" : ""} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts index a28029a38e..66104dfe8b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts @@ -1,5 +1,5 @@ import { QuestionType } from "@formbricks/types/questions"; -import { TSurvey } from "@formbricks/types/v1/surveys"; +import { TSurvey, TSurveyHiddenFields } from "@formbricks/types/v1/surveys"; import { TTemplate } from "@formbricks/types/v1/templates"; import { createId } from "@paralleldrive/cuid2"; @@ -9,6 +9,11 @@ const thankYouCardDefault = { subheader: "We appreciate your feedback.", }; +const hiddenFieldsDefault: TSurveyHiddenFields = { + enabled: true, + fieldIds: [], +}; + export const templates: TTemplate[] = [ { name: "Product Market Fit (Superhuman)", @@ -105,6 +110,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -207,6 +213,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -294,6 +301,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -358,6 +366,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -460,6 +469,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -506,6 +516,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -529,6 +540,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -624,6 +636,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -673,6 +686,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -714,6 +728,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -757,6 +772,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -820,6 +836,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, @@ -860,6 +877,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -897,6 +915,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -945,6 +964,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -1007,6 +1027,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -1048,6 +1069,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -1087,6 +1109,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -1131,6 +1154,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -1159,6 +1183,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -1201,6 +1226,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -1239,6 +1265,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, @@ -1290,6 +1317,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -1320,6 +1348,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -1370,6 +1399,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -1402,6 +1432,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, @@ -1445,6 +1476,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -1487,6 +1519,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -1529,6 +1562,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -1603,6 +1637,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -1724,6 +1759,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -1757,6 +1793,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -1807,6 +1844,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -1858,6 +1896,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -1956,6 +1995,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, { @@ -2053,6 +2093,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, @@ -2075,6 +2116,7 @@ export const templates: TTemplate[] = [ }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }, */ ]; @@ -2096,6 +2138,7 @@ export const customSurvey: TTemplate = { }, ], thankYouCard: thankYouCardDefault, + hiddenFields: hiddenFieldsDefault, }, }; @@ -2117,6 +2160,9 @@ export const minimalSurvey: TSurvey = { thankYouCard: { enabled: false, }, + hiddenFields: { + enabled: false, + }, delay: 0, // No delay autoComplete: null, closeOnDate: null, diff --git a/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx b/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx index 42e64f4cc2..2566bc3ebe 100644 --- a/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx +++ b/apps/web/app/s/[surveyId]/components/LinkSurvey.tsx @@ -80,6 +80,23 @@ export default function LinkSurvey({ } }, []); + const [hiddenFieldsRecord, setHiddenFieldsRecord] = useState>(); + + useEffect(() => { + survey.hiddenFields?.fieldIds?.forEach((field) => { + // set the question and answer to the survey state + const answer = searchParams?.get(field); + if (answer) { + setHiddenFieldsRecord((prev) => { + return { + ...prev, + [field]: answer, + }; + }); + } + }); + }, [searchParams, survey.hiddenFields?.fieldIds]); + useEffect(() => { responseQueue.updateSurveyState(surveyState); }, [responseQueue, surveyState]); @@ -123,7 +140,14 @@ export default function LinkSurvey({ } }} onResponse={(responseUpdate: TResponseUpdate) => { - !isPreview && responseQueue.add(responseUpdate); + !isPreview && + responseQueue.add({ + data: { + ...responseUpdate.data, + ...hiddenFieldsRecord, + }, + finished: responseUpdate.finished, + }); }} onActiveQuestionChange={(questionId) => setActiveQuestionId(questionId)} activeQuestionId={activeQuestionId} diff --git a/packages/database/jsonTypes.ts b/packages/database/jsonTypes.ts index f751e931ab..228ab6118a 100644 --- a/packages/database/jsonTypes.ts +++ b/packages/database/jsonTypes.ts @@ -1,8 +1,9 @@ import { TActionClassNoCodeConfig } from "@formbricks/types/v1/actionClasses"; import { TIntegrationConfig } from "@formbricks/types/v1/integrations"; -import { TResponsePersonAttributes, TResponseData, TResponseMeta } from "@formbricks/types/v1/responses"; +import { TResponseData, TResponseMeta, TResponsePersonAttributes } from "@formbricks/types/v1/responses"; import { TSurveyClosedMessage, + TSurveyHiddenFields, TSurveyProductOverwrites, TSurveyQuestions, TSurveySingleUse, @@ -21,6 +22,7 @@ declare global { export type ResponsePersonAttributes = TResponsePersonAttributes; export type SurveyQuestions = TSurveyQuestions; export type SurveyThankYouCard = TSurveyThankYouCard; + export type SurveyHiddenFields = TSurveyHiddenFields; export type SurveyProductOverwrites = TSurveyProductOverwrites; export type SurveyClosedMessage = TSurveyClosedMessage; export type SurveySingleUse = TSurveySingleUse; diff --git a/packages/database/migrations/20231015165646_add_hidden_fields/migration.sql b/packages/database/migrations/20231015165646_add_hidden_fields/migration.sql new file mode 100644 index 0000000000..9eb2c067b0 --- /dev/null +++ b/packages/database/migrations/20231015165646_add_hidden_fields/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Survey" ADD COLUMN "hiddenFields" JSONB NOT NULL DEFAULT '{"enabled": false}'; diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma index 8576c28a4f..ea28f51f1c 100644 --- a/packages/database/schema.prisma +++ b/packages/database/schema.prisma @@ -240,6 +240,9 @@ model Survey { /// @zod.custom(imports.ZSurveyThankYouCard) /// [SurveyThankYouCard] thankYouCard Json @default("{\"enabled\": false}") + /// @zod.custom(imports.ZSurveyHiddenFields) + /// [SurveyHiddenFields] + hiddenFields Json @default("{\"enabled\": false}") responses Response[] displayOption displayOptions @default(displayOnce) recontactDays Int? diff --git a/packages/database/zod-utils.ts b/packages/database/zod-utils.ts index 9106c3f1bb..1e61c98012 100644 --- a/packages/database/zod-utils.ts +++ b/packages/database/zod-utils.ts @@ -9,6 +9,7 @@ export { ZResponseData, ZResponsePersonAttributes, ZResponseMeta } from "@formbr export { ZSurveyQuestions, ZSurveyThankYouCard, + ZSurveyHiddenFields, ZSurveyClosedMessage, ZSurveyProductOverwrites, ZSurveyVerifyEmail, diff --git a/packages/lib/survey/service.ts b/packages/lib/survey/service.ts index 64bcb43e53..6a286846d7 100644 --- a/packages/lib/survey/service.ts +++ b/packages/lib/survey/service.ts @@ -1,26 +1,26 @@ import "server-only"; import { prisma } from "@formbricks/database"; +import { ZString } from "@formbricks/types/v1/common"; import { ZId } from "@formbricks/types/v1/environment"; import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/v1/errors"; import { TSurvey, TSurveyAttributeFilter, + TSurveyInput, TSurveyWithAnalytics, ZSurvey, ZSurveyWithAnalytics, - TSurveyInput, } from "@formbricks/types/v1/surveys"; import { Prisma } from "@prisma/client"; import { revalidateTag, unstable_cache } from "next/cache"; import { z } from "zod"; -import { captureTelemetry } from "../telemetry"; -import { validateInputs } from "../utils/validate"; +import { getActionClasses } from "../actionClass/service"; +import { SERVICES_REVALIDATION_INTERVAL } from "../constants"; import { getDisplaysCacheTag } from "../display/service"; import { getResponsesCacheTag } from "../response/service"; -import { ZString } from "@formbricks/types/v1/common"; -import { SERVICES_REVALIDATION_INTERVAL } from "../constants"; -import { getActionClasses } from "../actionClass/service"; +import { captureTelemetry } from "../telemetry"; +import { validateInputs } from "../utils/validate"; import { formatSurveyDateFields } from "./util"; // surveys cache key and tags @@ -39,6 +39,7 @@ export const selectSurvey = { status: true, questions: true, thankYouCard: true, + hiddenFields: true, displayOption: true, recontactDays: true, autoClose: true, diff --git a/packages/types/v1/surveys.ts b/packages/types/v1/surveys.ts index c4199d313f..88cd824fc6 100644 --- a/packages/types/v1/surveys.ts +++ b/packages/types/v1/surveys.ts @@ -8,6 +8,11 @@ export const ZSurveyThankYouCard = z.object({ subheader: z.optional(z.string()), }); +export const ZSurveyHiddenFields = z.object({ + enabled: z.boolean(), + fieldIds: z.optional(z.array(z.string())), +}); + export const ZSurveyProductOverwrites = z.object({ brandColor: ZColor.nullish(), highlightBorderColor: ZColor.nullish(), @@ -49,6 +54,8 @@ export type TSurveyVerifyEmail = z.infer; export type TSurveyThankYouCard = z.infer; +export type TSurveyHiddenFields = z.infer; + export type TSurveyClosedMessage = z.infer; export const ZSurveyChoice = z.object({ @@ -280,6 +287,7 @@ export const ZSurvey = z.object({ recontactDays: z.number().nullable(), questions: ZSurveyQuestions, thankYouCard: ZSurveyThankYouCard, + hiddenFields: ZSurveyHiddenFields, delay: z.number(), autoComplete: z.number().nullable(), closeOnDate: z.date().nullable(), @@ -299,6 +307,7 @@ export const ZSurveyInput = z.object({ recontactDays: z.number().optional(), questions: ZSurveyQuestions.optional(), thankYouCard: ZSurveyThankYouCard.optional(), + hiddenFields: ZSurveyHiddenFields, delay: z.number().optional(), autoComplete: z.number().optional(), closeOnDate: z.date().optional(), diff --git a/packages/types/v1/templates.ts b/packages/types/v1/templates.ts index 9bc8f3e84b..ba08df98ed 100644 --- a/packages/types/v1/templates.ts +++ b/packages/types/v1/templates.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { ZSurveyQuestions, ZSurveyThankYouCard } from "./surveys"; +import { ZSurveyHiddenFields, ZSurveyQuestions, ZSurveyThankYouCard } from "./surveys"; const ZTemplateObjective = z.enum([ "increase_user_adoption", @@ -22,6 +22,7 @@ export const ZTemplate = z.object({ name: z.string(), questions: ZSurveyQuestions, thankYouCard: ZSurveyThankYouCard, + hiddenFields: ZSurveyHiddenFields, }), }); diff --git a/packages/ui/SingleResponseCard/index.tsx b/packages/ui/SingleResponseCard/index.tsx index 63f69ed475..fe9766b1f4 100644 --- a/packages/ui/SingleResponseCard/index.tsx +++ b/packages/ui/SingleResponseCard/index.tsx @@ -310,6 +310,18 @@ export default function SingleResponseCard({ ); })} + {survey.hiddenFields?.enabled && survey.hiddenFields?.fieldIds?.length && ( +
+ {survey.hiddenFields.fieldIds.map((field) => { + return ( +
+

Hidden Field: {field}

+

{response.data[field]}

+
+ ); + })} +
+ )} {response.finished && (
diff --git a/packages/ui/Tag/index.tsx b/packages/ui/Tag/index.tsx index d3ee7cd568..7604300070 100644 --- a/packages/ui/Tag/index.tsx +++ b/packages/ui/Tag/index.tsx @@ -7,29 +7,15 @@ interface Tag { } interface ResponseTagsWrapperProps { - tags: Tag[]; tagId: string; tagName: string; onDelete: (tagId: string) => void; - setTagsState: (tags: Tag[]) => void; + tags?: Tag[]; + setTagsState?: (tags: Tag[]) => void; highlight?: boolean; } -export function Tag({ - tagId, - tagName, - onDelete, - tags, - setTagsState, - highlight, -}: { - tagId: string; - tagName: string; - onDelete: (tagId: string) => void; - tags: ResponseTagsWrapperProps["tags"]; - setTagsState: (tags: ResponseTagsWrapperProps["tags"]) => void; - highlight?: boolean; -}) { +export function Tag({ tagId, tagName, onDelete, tags, setTagsState, highlight }: ResponseTagsWrapperProps) { return (
{ - setTagsState(tags.filter((tag) => tag.tagId !== tagId)); + if (tags && setTagsState) setTagsState(tags.filter((tag) => tag.tagId !== tagId)); onDelete(tagId); }}>