diff --git a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceMultiForm.tsx b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceMultiForm.tsx
index e3a175320d..cfcf479e4e 100644
--- a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceMultiForm.tsx
+++ b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceMultiForm.tsx
@@ -3,6 +3,7 @@ import { Survey } from "@formbricks/types/surveys";
import { Button, Input, Label } from "@formbricks/ui";
import { TrashIcon } from "@heroicons/react/24/solid";
import { createId } from "@paralleldrive/cuid2";
+import { cn } from "@formbricks/lib/cn";
interface OpenQuestionFormProps {
localSurvey: Survey;
@@ -31,11 +32,26 @@ export default function MultipleChoiceMultiForm({
};
const addChoice = () => {
- const newChoices = !question.choices ? [] : question.choices;
+ let newChoices = !question.choices ? [] : question.choices;
+ const otherChoice = newChoices.find((choice) => choice.id === "other");
+ if (otherChoice) {
+ newChoices = newChoices.filter((choice) => choice.id !== "other");
+ }
newChoices.push({ id: createId(), label: "" });
+ if (otherChoice) {
+ newChoices.push(otherChoice);
+ }
updateQuestion(questionIdx, { choices: newChoices });
};
+ const addOther = () => {
+ if (question.choices.filter((c) => c.id === "other").length === 0) {
+ const newChoices = !question.choices ? [] : question.choices.filter((c) => c.id !== "other");
+ newChoices.push({ id: "other", label: "Other" });
+ updateQuestion(questionIdx, { choices: newChoices });
+ }
+ };
+
const deleteChoice = (choiceIdx: number) => {
const newChoices = !question.choices ? [] : question.choices.filter((_, idx) => idx !== choiceIdx);
@@ -90,7 +106,8 @@ export default function MultipleChoiceMultiForm({
id={choice.id}
name={choice.id}
value={choice.label}
- placeholder={`Option ${choiceIdx + 1}`}
+ className={cn(choice.id === "other" && "border-dashed")}
+ placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })}
/>
{question.choices && question.choices.length > 2 && (
@@ -101,9 +118,19 @@ export default function MultipleChoiceMultiForm({
)}
))}
-
+
+
+ {question.choices.filter((c) => c.id === "other").length === 0 && (
+ <>
+
or
+
+ >
+ )}
+
diff --git a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceSingleForm.tsx b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceSingleForm.tsx
index fcc4288024..515a34c7d8 100644
--- a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceSingleForm.tsx
+++ b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceSingleForm.tsx
@@ -3,6 +3,7 @@ import { Survey } from "@formbricks/types/surveys";
import { Button, Input, Label } from "@formbricks/ui";
import { TrashIcon } from "@heroicons/react/24/solid";
import { createId } from "@paralleldrive/cuid2";
+import { cn } from "@formbricks/lib/cn";
interface OpenQuestionFormProps {
localSurvey: Survey;
@@ -31,11 +32,26 @@ export default function MultipleChoiceSingleForm({
};
const addChoice = () => {
- const newChoices = !question.choices ? [] : question.choices;
+ let newChoices = !question.choices ? [] : question.choices;
+ const otherChoice = newChoices.find((choice) => choice.id === "other");
+ if (otherChoice) {
+ newChoices = newChoices.filter((choice) => choice.id !== "other");
+ }
newChoices.push({ id: createId(), label: "" });
+ if (otherChoice) {
+ newChoices.push(otherChoice);
+ }
updateQuestion(questionIdx, { choices: newChoices });
};
+ const addOther = () => {
+ if (question.choices.filter((c) => c.id === "other").length === 0) {
+ const newChoices = !question.choices ? [] : question.choices.filter((c) => c.id !== "other");
+ newChoices.push({ id: "other", label: "Other" });
+ updateQuestion(questionIdx, { choices: newChoices });
+ }
+ };
+
const deleteChoice = (choiceIdx: number) => {
const newChoices = !question.choices ? [] : question.choices.filter((_, idx) => idx !== choiceIdx);
@@ -90,7 +106,8 @@ export default function MultipleChoiceSingleForm({
id={choice.id}
name={choice.id}
value={choice.label}
- placeholder={`Option ${choiceIdx + 1}`}
+ className={cn(choice.id === "other" && "border-dashed")}
+ placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })}
/>
{question.choices && question.choices.length > 2 && (
@@ -101,9 +118,19 @@ export default function MultipleChoiceSingleForm({
)}
))}
-
+
+
+ {question.choices.filter((c) => c.id === "other").length === 0 && (
+ <>
+
or
+
+ >
+ )}
+
diff --git a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/MultipleChoiceSummary.tsx b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/MultipleChoiceSummary.tsx
index cd2da50b61..8cf8b1b5fa 100644
--- a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/MultipleChoiceSummary.tsx
+++ b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/MultipleChoiceSummary.tsx
@@ -1,43 +1,93 @@
import { MultipleChoiceMultiQuestion, MultipleChoiceSingleQuestion } from "@formbricks/types/questions";
import type { QuestionSummary } from "@formbricks/types/responses";
-import { ProgressBar } from "@formbricks/ui";
+import { PersonAvatar, ProgressBar } from "@formbricks/ui";
import { InboxStackIcon } from "@heroicons/react/24/solid";
import { useMemo } from "react";
+import Link from "next/link";
+import { truncate } from "@/lib/utils";
interface MultipleChoiceSummaryProps {
questionSummary: QuestionSummary;
+ environmentId: string;
+ surveyType: string;
}
interface ChoiceResult {
+ id: string;
label: string;
count: number;
percentage?: number;
+ otherValues?: {
+ value: string;
+ person: {
+ id: string;
+ name?: string;
+ email?: string;
+ };
+ }[];
}
-export default function MultipleChoiceSummary({ questionSummary }: MultipleChoiceSummaryProps) {
+export default function MultipleChoiceSummary({
+ questionSummary,
+ environmentId,
+ surveyType,
+}: MultipleChoiceSummaryProps) {
const isSingleChoice = questionSummary.question.type === "multipleChoiceSingle";
const results: ChoiceResult[] = useMemo(() => {
if (!("choices" in questionSummary.question)) return [];
+ console.log(questionSummary.responses);
// build a dictionary of choices
const resultsDict: { [key: string]: ChoiceResult } = {};
for (const choice of questionSummary.question.choices) {
resultsDict[choice.label] = {
- count: 0,
+ id: choice.id,
label: choice.label,
+ count: 0,
percentage: 0,
+ otherValues: [],
};
}
+
+ function findEmail(person) {
+ const emailAttribute = person.attributes.find((attr) => attr.attributeClass.name === "email");
+ return emailAttribute ? emailAttribute.value : null;
+ }
+
+ const addOtherChoice = (response, value) => {
+ for (const key in resultsDict) {
+ if (resultsDict[key].id === "other" && value !== "") {
+ const email = response.person && findEmail(response.person);
+ const displayIdentifier = email || truncate(response.personId, 16);
+ resultsDict[key].otherValues?.push({
+ value,
+ person: {
+ id: response.personId,
+ email: displayIdentifier,
+ },
+ });
+ resultsDict[key].count += 1;
+ break;
+ }
+ }
+ };
+
// count the responses
for (const response of questionSummary.responses) {
// if single choice, only add responses that are in the choices
if (isSingleChoice && response.value in resultsDict) {
resultsDict[response.value].count += 1;
+ } else if (isSingleChoice) {
+ // if single choice and not in choices, add to other
+ addOtherChoice(response, response.value);
} else {
// if multi choice add all responses
for (const choice of response.value) {
if (choice in resultsDict) {
resultsDict[choice].count += 1;
+ } else {
+ // if multi choice and not in choices, add to other
+ addOtherChoice(response, choice);
}
}
}
@@ -49,8 +99,15 @@ export default function MultipleChoiceSummary({ questionSummary }: MultipleChoic
resultsDict[key].percentage = resultsDict[key].count / total;
}
}
+
// sort by count and transform to array
- const results = Object.values(resultsDict).sort((a: any, b: any) => b.count - a.count);
+ const results = Object.values(resultsDict).sort((a: any, b: any) => {
+ if (a.id === "other") return 1; // Always put a after b if a's id is 'other'
+ if (b.id === "other") return -1; // Always put b after a if b's id is 'other'
+
+ // If neither id is 'other', compare counts
+ return b.count - a.count;
+ });
return results;
}, [questionSummary, isSingleChoice]);
@@ -103,6 +160,45 @@ export default function MultipleChoiceSummary({ questionSummary }: MultipleChoic
+ {result.otherValues.length > 0 && (
+
+
+
Specified "Other" answers
+
{surveyType === "web" && "User"}
+
+ {result.otherValues
+ .filter((otherValue) => otherValue !== "")
+ .map((otherValue, idx) => (
+
+ {surveyType === "link" && (
+
+ {otherValue.value}
+
+ )}
+ {surveyType === "web" && (
+
+
+ {otherValue.value}
+
+
+ {otherValue.person.id &&
}
+
{otherValue.person.email}
+
+
+ )}
+
+ ))}
+
+ )}
))}
diff --git a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/SummaryList.tsx b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/SummaryList.tsx
index 57fd3ee007..66844733ba 100644
--- a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/SummaryList.tsx
+++ b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/summary/SummaryList.tsx
@@ -90,6 +90,8 @@ export default function SummaryList({ environmentId, surveyId }) {
MultipleChoiceMultiQuestion | MultipleChoiceSingleQuestion
>
}
+ environmentId={environmentId}
+ surveyType={survey.type}
/>
);
}
diff --git a/apps/web/components/preview/MultipleChoiceMultiQuestion.tsx b/apps/web/components/preview/MultipleChoiceMultiQuestion.tsx
index 3dcf407087..dffe7e9063 100644
--- a/apps/web/components/preview/MultipleChoiceMultiQuestion.tsx
+++ b/apps/web/components/preview/MultipleChoiceMultiQuestion.tsx
@@ -4,6 +4,7 @@ import type { MultipleChoiceMultiQuestion } from "@formbricks/types/questions";
import Headline from "./Headline";
import Subheader from "./Subheader";
import SubmitButton from "@/components/preview/SubmitButton";
+import { Input } from "@/../../packages/ui";
interface MultipleChoiceMultiProps {
question: MultipleChoiceMultiQuestion;
@@ -20,16 +21,22 @@ export default function MultipleChoiceMultiQuestion({
}: MultipleChoiceMultiProps) {
const [selectedChoices, setSelectedChoices] = useState([]);
const [isAtLeastOneChecked, setIsAtLeastOneChecked] = useState(false);
+ const [showOther, setShowOther] = useState(false);
+ const [otherSpecified, setOtherSpecified] = useState("");
useEffect(() => {
- setIsAtLeastOneChecked(selectedChoices.length > 0);
- }, [selectedChoices]);
+ setIsAtLeastOneChecked(selectedChoices.length > 0 || otherSpecified.length > 0);
+ }, [selectedChoices, otherSpecified]);
return (