fix: multi and single select questions (#2606)

This commit is contained in:
Anshuman Pandey
2024-05-14 13:56:26 +05:30
committed by GitHub
parent a91a5e7014
commit 5da9091aee
16 changed files with 96 additions and 485 deletions

View File

@@ -1,360 +0,0 @@
"use client";
import { createId } from "@paralleldrive/cuid2";
import { PlusIcon, TrashIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { toast } from "react-hot-toast";
import {
createI18nString,
extractLanguageCodes,
isLabelValidForAllLanguages,
} from "@formbricks/lib/i18n/utils";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import {
TI18nString,
TShuffleOption,
TSurvey,
TSurveyMultipleChoiceMultiQuestion,
TSurveyQuestionType,
} from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { Label } from "@formbricks/ui/Label";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
interface OpenQuestionFormProps {
localSurvey: TSurvey;
question: TSurveyMultipleChoiceMultiQuestion;
questionIdx: number;
updateQuestion: (
questionIdx: number,
updatedAttributes: Partial<TSurveyMultipleChoiceMultiQuestion>
) => void;
lastQuestion: boolean;
selectedLanguageCode: string;
setSelectedLanguageCode: (languageCode: string) => void;
isInvalid: boolean;
}
export const MultipleChoiceMultiForm = ({
question,
questionIdx,
updateQuestion,
isInvalid,
localSurvey,
selectedLanguageCode,
setSelectedLanguageCode,
}: OpenQuestionFormProps): JSX.Element => {
const lastChoiceRef = useRef<HTMLInputElement>(null);
const [isNew, setIsNew] = useState(true);
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
const questionRef = useRef<HTMLInputElement>(null);
const [isInvalidValue, setisInvalidValue] = useState<string | null>(null);
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const shuffleOptionsTypes = {
none: {
id: "none",
label: "Keep current order",
show: true,
},
all: {
id: "all",
label: "Randomize all",
show: question.choices.filter((c) => c.id === "other").length === 0,
},
exceptLast: {
id: "exceptLast",
label: "Randomize all except last option",
show: true,
},
};
const updateChoice = (choiceIdx: number, updatedAttributes: { label: TI18nString }) => {
const newLabel = updatedAttributes.label.en;
const oldLabel = question.choices[choiceIdx].label;
let newChoices: any[] = [];
if (question.choices) {
newChoices = question.choices.map((choice, idx) => {
if (idx !== choiceIdx) return choice;
return { ...choice, ...updatedAttributes };
});
}
let newLogic: any[] = [];
question.logic?.forEach((logic) => {
let newL: string | string[] | undefined = logic.value;
if (Array.isArray(logic.value)) {
newL = logic.value.map((value) =>
newLabel && value === oldLabel[selectedLanguageCode] ? newLabel : value
);
} else {
newL = logic.value === oldLabel[selectedLanguageCode] ? newLabel : logic.value;
}
newLogic.push({ ...logic, value: newL });
});
updateQuestion(questionIdx, { choices: newChoices, logic: newLogic });
};
const findDuplicateLabel = () => {
for (let i = 0; i < question.choices.length; i++) {
for (let j = i + 1; j < question.choices.length; j++) {
if (
getLocalizedValue(question.choices[i].label, selectedLanguageCode).trim() ===
getLocalizedValue(question.choices[j].label, selectedLanguageCode).trim()
) {
return getLocalizedValue(question.choices[i].label, selectedLanguageCode).trim(); // Return the duplicate label
}
}
}
return null;
};
const addChoice = (choiceIdx?: number) => {
setIsNew(false); // This question is no longer new.
let newChoices = !question.choices ? [] : question.choices;
const otherChoice = newChoices.find((choice) => choice.id === "other");
if (otherChoice) {
newChoices = newChoices.filter((choice) => choice.id !== "other");
}
const newChoice = {
id: createId(),
label: createI18nString("", surveyLanguageCodes),
};
if (choiceIdx !== undefined) {
newChoices.splice(choiceIdx + 1, 0, newChoice);
} else {
newChoices.push(newChoice);
}
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: createI18nString("Other", surveyLanguageCodes),
});
updateQuestion(questionIdx, {
choices: newChoices,
...(question.shuffleOption === shuffleOptionsTypes.all.id && {
shuffleOption: shuffleOptionsTypes.exceptLast.id as TShuffleOption,
}),
});
}
};
const deleteChoice = (choiceIdx: number) => {
const newChoices = !question.choices ? [] : question.choices.filter((_, idx) => idx !== choiceIdx);
const choiceValue = question.choices[choiceIdx].label[selectedLanguageCode];
if (isInvalidValue === choiceValue) {
setisInvalidValue(null);
}
let newLogic: any[] = [];
question.logic?.forEach((logic) => {
let newL: string | string[] | undefined = logic.value;
if (Array.isArray(logic.value)) {
newL = logic.value.filter((value) => value !== choiceValue);
} else {
newL = logic.value !== choiceValue ? logic.value : undefined;
}
newLogic.push({ ...logic, value: newL });
});
updateQuestion(questionIdx, { choices: newChoices, logic: newLogic });
};
useEffect(() => {
if (lastChoiceRef.current) {
lastChoiceRef.current?.focus();
}
}, [question.choices?.length]);
// This effect will run once on initial render, setting focus to the question input.
useEffect(() => {
if (isNew && questionRef.current) {
questionRef.current.focus();
}
}, [isNew]);
return (
<form>
<QuestionFormInput
id="headline"
value={question.headline}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
/>
<div>
{showSubheader && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
id="subheader"
value={question.subheader}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
/>
</div>
<TrashIcon
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
onClick={() => {
setShowSubheader(false);
updateQuestion(questionIdx, { subheader: undefined });
}}
/>
</div>
)}
{!showSubheader && (
<Button
size="sm"
variant="minimal"
className="mt-3"
type="button"
onClick={() => {
updateQuestion(questionIdx, {
subheader: createI18nString("", surveyLanguageCodes),
});
setShowSubheader(true);
}}>
{" "}
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
</Button>
)}
</div>
<div className="mt-3">
<Label htmlFor="choices">Options</Label>
<div className="mt-2 -space-y-2" id="choices">
{question.choices &&
question.choices.map((choice, choiceIdx) => (
<div key={choiceIdx} className="inline-flex w-full items-center">
<div className="flex w-full space-x-2">
<QuestionFormInput
key={choice.id}
id={`choice-${choiceIdx}`}
localSurvey={localSurvey}
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
questionIdx={questionIdx}
value={choice.label}
onBlur={() => {
const duplicateLabel = findDuplicateLabel();
if (duplicateLabel) {
toast.error("Duplicate choices");
setisInvalidValue(duplicateLabel);
} else {
setisInvalidValue(null);
}
}}
updateChoice={updateChoice}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={
isInvalid &&
!isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguageCodes)
}
className={`${choice.id === "other" ? "border border-dashed" : ""}`}
/>
{choice.id === "other" && (
<QuestionFormInput
id="otherOptionPlaceholder"
localSurvey={localSurvey}
placeholder={"Please specify"}
questionIdx={questionIdx}
value={
question.otherOptionPlaceholder
? question.otherOptionPlaceholder
: createI18nString("Please specify", surveyLanguageCodes)
}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={
isInvalid &&
!isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguageCodes)
}
className="border border-dashed"
/>
)}
</div>
{question.choices && question.choices.length > 2 && (
<TrashIcon
className="ml-2 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
onClick={() => deleteChoice(choiceIdx)}
/>
)}
<div className="ml-2 h-4 w-4">
{choice.id !== "other" && (
<PlusIcon
className="h-full w-full cursor-pointer text-slate-400 hover:text-slate-500"
onClick={() => addChoice(choiceIdx)}
/>
)}
</div>
</div>
))}
<div className="flex items-center justify-between space-x-2">
{question.choices.filter((c) => c.id === "other").length === 0 && (
<Button size="sm" variant="minimal" type="button" onClick={() => addOther()}>
Add &quot;Other&quot;
</Button>
)}
<Button
size="sm"
variant="minimal"
type="button"
onClick={() => {
// @ts-expect-error
updateQuestion(questionIdx, { type: TSurveyQuestionType.MultipleChoiceSingle });
}}>
Convert to Single Select
</Button>
<div className="flex flex-1 items-center justify-end gap-2">
<Select
defaultValue={question.shuffleOption}
value={question.shuffleOption}
onValueChange={(e: TShuffleOption) => {
updateQuestion(questionIdx, { shuffleOption: e });
}}>
<SelectTrigger className="w-fit space-x-2 overflow-hidden border-0 font-semibold text-slate-600">
<SelectValue placeholder="Select ordering" />
</SelectTrigger>
<SelectContent>
{Object.values(shuffleOptionsTypes).map(
(shuffleOptionsType) =>
shuffleOptionsType.show && (
<SelectItem
key={shuffleOptionsType.id}
value={shuffleOptionsType.id}
title={shuffleOptionsType.label}>
{shuffleOptionsType.label}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
</form>
);
};

View File

@@ -11,7 +11,7 @@ import {
TI18nString,
TShuffleOption,
TSurvey,
TSurveyMultipleChoiceSingleQuestion,
TSurveyMultipleChoiceQuestion,
TSurveyQuestionType,
} from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
@@ -23,19 +23,16 @@ import { isLabelValidForAllLanguages } from "../lib/validation";
interface OpenQuestionFormProps {
localSurvey: TSurvey;
question: TSurveyMultipleChoiceSingleQuestion;
question: TSurveyMultipleChoiceQuestion;
questionIdx: number;
updateQuestion: (
questionIdx: number,
updatedAttributes: Partial<TSurveyMultipleChoiceSingleQuestion>
) => void;
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyMultipleChoiceQuestion>) => void;
lastQuestion: boolean;
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
}
export const MultipleChoiceSingleForm = ({
export const MultipleChoiceQuestionForm = ({
question,
questionIdx,
updateQuestion,
@@ -48,6 +45,7 @@ export const MultipleChoiceSingleForm = ({
const [isNew, setIsNew] = useState(true);
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
const [isInvalidValue, setisInvalidValue] = useState<string | null>(null);
const questionRef = useRef<HTMLInputElement>(null);
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const surveyLanguages = localSurvey.languages ?? [];
@@ -318,10 +316,15 @@ export const MultipleChoiceSingleForm = ({
variant="minimal"
type="button"
onClick={() => {
// @ts-expect-error
updateQuestion(questionIdx, { type: TSurveyQuestionType.MultipleChoiceMulti });
updateQuestion(questionIdx, {
type:
question.type === TSurveyQuestionType.MultipleChoiceMulti
? TSurveyQuestionType.MultipleChoiceSingle
: TSurveyQuestionType.MultipleChoiceMulti,
});
}}>
Convert to Multi Select
Convert to {question.type === TSurveyQuestionType.MultipleChoiceSingle ? "Multiple" : "Single"}{" "}
Select
</Button>
<div className="flex flex-1 items-center justify-end gap-2">

View File

@@ -38,8 +38,7 @@ import { ConsentQuestionForm } from "./ConsentQuestionForm";
import { DateQuestionForm } from "./DateQuestionForm";
import { FileUploadQuestionForm } from "./FileUploadQuestionForm";
import { MatrixQuestionForm } from "./MatrixQuestionForm";
import { MultipleChoiceMultiForm } from "./MultipleChoiceMultiForm";
import { MultipleChoiceSingleForm } from "./MultipleChoiceSingleForm";
import { MultipleChoiceQuestionForm } from "./MultipleChoiceQuestionForm";
import { NPSQuestionForm } from "./NPSQuestionForm";
import { OpenQuestionForm } from "./OpenQuestionForm";
import { PictureSelectionForm } from "./PictureSelectionForm";
@@ -239,7 +238,7 @@ export default function QuestionCard({
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
<MultipleChoiceSingleForm
<MultipleChoiceQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
@@ -250,7 +249,7 @@ export default function QuestionCard({
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.MultipleChoiceMulti ? (
<MultipleChoiceMultiForm
<MultipleChoiceQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}

View File

@@ -12,8 +12,7 @@ import {
TSurveyConsentQuestion,
TSurveyLanguage,
TSurveyMatrixQuestion,
TSurveyMultipleChoiceMultiQuestion,
TSurveyMultipleChoiceSingleQuestion,
TSurveyMultipleChoiceQuestion,
TSurveyOpenTextQuestion,
TSurveyPictureSelectionQuestion,
TSurveyQuestion,
@@ -38,7 +37,7 @@ export const isLabelValidForAllLanguages = (
// Validation logic for multiple choice questions
const handleI18nCheckForMultipleChoice = (
question: TSurveyMultipleChoiceMultiQuestion | TSurveyMultipleChoiceSingleQuestion,
question: TSurveyMultipleChoiceQuestion,
languages: TSurveyLanguage[]
): boolean => {
return question.choices.every((choice) => isLabelValidForAllLanguages(choice.label, languages));
@@ -84,10 +83,10 @@ export const validationRules = {
? isLabelValidForAllLanguages(question.placeholder, languages)
: true;
},
multipleChoiceMulti: (question: TSurveyMultipleChoiceMultiQuestion, languages: TSurveyLanguage[]) => {
multipleChoiceMulti: (question: TSurveyMultipleChoiceQuestion, languages: TSurveyLanguage[]) => {
return handleI18nCheckForMultipleChoice(question, languages);
},
multipleChoiceSingle: (question: TSurveyMultipleChoiceSingleQuestion, languages: TSurveyLanguage[]) => {
multipleChoiceSingle: (question: TSurveyMultipleChoiceQuestion, languages: TSurveyLanguage[]) => {
return handleI18nCheckForMultipleChoice(question, languages);
},
consent: (question: TSurveyConsentQuestion, languages: TSurveyLanguage[]) => {

View File

@@ -22,19 +22,6 @@ export const TeamSettingsNavbar = ({
const { isAdmin, isOwner } = getAccessFlags(membershipRole);
const isPricingDisabled = !isOwner && !isAdmin;
console.log({
environmentId,
isFormbricksCloud,
membershipRole,
activeId,
pathname,
isAdmin,
isOwner,
isPricingDisabled,
});
console.log("hidden: ", !isFormbricksCloud || isPricingDisabled);
const navigation = [
{
id: "members",

View File

@@ -24,8 +24,7 @@ import {
TSurveyDateQuestion,
TSurveyFileUploadQuestion,
TSurveyMatrixQuestion,
TSurveyMultipleChoiceMultiQuestion,
TSurveyMultipleChoiceSingleQuestion,
TSurveyMultipleChoiceQuestion,
TSurveyNPSQuestion,
TSurveyOpenTextQuestion,
TSurveyPictureSelectionQuestion,
@@ -69,7 +68,7 @@ export const questionTypes: TSurveyQuestionType[] = [
{ id: createId(), label: { default: "Have the cake 🎂" } },
],
shuffleOption: "none",
} as Partial<TSurveyMultipleChoiceSingleQuestion>,
} as Partial<TSurveyMultipleChoiceQuestion>,
},
{
id: QuestionId.MultipleChoiceMulti,
@@ -84,7 +83,7 @@ export const questionTypes: TSurveyQuestionType[] = [
{ id: createId(), label: { default: "Palms 🌴" } },
],
shuffleOption: "none",
} as Partial<TSurveyMultipleChoiceMultiQuestion>,
} as Partial<TSurveyMultipleChoiceQuestion>,
},
{
id: QuestionId.PictureSelection,

View File

@@ -7,12 +7,14 @@ import {
import { TLanguage } from "@formbricks/types/product";
import {
TI18nString,
TSurvey,
TSurveyCTAQuestion,
TSurveyChoice,
TSurveyConsentQuestion,
TSurveyMultipleChoiceSingleQuestion,
TSurveyMultipleChoiceQuestion,
TSurveyNPSQuestion,
TSurveyOpenTextQuestion,
TSurveyQuestion,
TSurveyQuestions,
TSurveyRatingQuestion,
TSurveyThankYouCard,
@@ -21,8 +23,7 @@ import {
ZSurveyCalQuestion,
ZSurveyConsentQuestion,
ZSurveyFileUploadQuestion,
ZSurveyMultipleChoiceMultiQuestion,
ZSurveyMultipleChoiceSingleQuestion,
ZSurveyMultipleChoiceQuestion,
ZSurveyNPSQuestion,
ZSurveyOpenTextQuestion,
ZSurveyPictureSelectionQuestion,
@@ -31,7 +32,6 @@ import {
ZSurveyThankYouCard,
ZSurveyWelcomeCard,
} from "@formbricks/types/surveys";
import { TSurvey, TSurveyMultipleChoiceMultiQuestion, TSurveyQuestion } from "@formbricks/types/surveys";
// Helper function to create an i18nString from a regular string.
export const createI18nString = (text: string | TI18nString, languages: string[]): TI18nString => {
@@ -160,21 +160,17 @@ const translateQuestion = (
case "multipleChoiceSingle":
case "multipleChoiceMulti":
(clonedQuestion as TSurveyMultipleChoiceSingleQuestion | TSurveyMultipleChoiceMultiQuestion).choices =
question.choices.map((choice) => {
return translateChoice(choice, languages);
});
if (
typeof (clonedQuestion as TSurveyMultipleChoiceSingleQuestion | TSurveyMultipleChoiceMultiQuestion)
.otherOptionPlaceholder !== "undefined"
) {
(
clonedQuestion as TSurveyMultipleChoiceSingleQuestion | TSurveyMultipleChoiceMultiQuestion
).otherOptionPlaceholder = createI18nString(question.otherOptionPlaceholder ?? "", languages);
(clonedQuestion as TSurveyMultipleChoiceQuestion).choices = question.choices.map((choice) => {
return translateChoice(choice, languages);
});
if (typeof (clonedQuestion as TSurveyMultipleChoiceQuestion).otherOptionPlaceholder !== "undefined") {
(clonedQuestion as TSurveyMultipleChoiceQuestion).otherOptionPlaceholder = createI18nString(
question.otherOptionPlaceholder ?? "",
languages
);
}
if (question.type === "multipleChoiceSingle") {
return ZSurveyMultipleChoiceSingleQuestion.parse(clonedQuestion);
} else return ZSurveyMultipleChoiceMultiQuestion.parse(clonedQuestion);
return ZSurveyMultipleChoiceQuestion.parse(clonedQuestion);
case "cta":
if (typeof question.dismissButtonLabel !== "undefined") {

View File

@@ -9,8 +9,7 @@ import {
TSurveyConsentQuestion,
TSurveyDateQuestion,
TSurveyFileUploadQuestion,
TSurveyMultipleChoiceMultiQuestion,
TSurveyMultipleChoiceSingleQuestion,
TSurveyMultipleChoiceQuestion,
TSurveyNPSQuestion,
TSurveyOpenTextQuestion,
TSurveyPictureSelectionQuestion,
@@ -50,7 +49,7 @@ export const mockOpenTextQuestion: TSurveyOpenTextQuestion = {
},
};
export const mockSingleSelectQuestion: TSurveyMultipleChoiceSingleQuestion = {
export const mockSingleSelectQuestion: TSurveyMultipleChoiceQuestion = {
id: "mvqx8t90np6isb6oel9eamzc",
type: TSurveyQuestionType.MultipleChoiceSingle,
choices: [
@@ -78,7 +77,7 @@ export const mockSingleSelectQuestion: TSurveyMultipleChoiceSingleQuestion = {
shuffleOption: "none",
};
export const mockMultiSelectQuestion: TSurveyMultipleChoiceMultiQuestion = {
export const mockMultiSelectQuestion: TSurveyMultipleChoiceQuestion = {
required: true,
headline: {
default: "What's important on vacay?",

View File

@@ -7,12 +7,15 @@ import {
import { TLanguage } from "@formbricks/types/product";
import {
TI18nString,
TSurvey,
TSurveyCTAQuestion,
TSurveyChoice,
TSurveyConsentQuestion,
TSurveyMultipleChoiceSingleQuestion,
TSurveyLanguage,
TSurveyMultipleChoiceQuestion,
TSurveyNPSQuestion,
TSurveyOpenTextQuestion,
TSurveyQuestion,
TSurveyRatingQuestion,
TSurveyThankYouCard,
TSurveyWelcomeCard,
@@ -20,8 +23,7 @@ import {
ZSurveyCalQuestion,
ZSurveyConsentQuestion,
ZSurveyFileUploadQuestion,
ZSurveyMultipleChoiceMultiQuestion,
ZSurveyMultipleChoiceSingleQuestion,
ZSurveyMultipleChoiceQuestion,
ZSurveyNPSQuestion,
ZSurveyOpenTextQuestion,
ZSurveyPictureSelectionQuestion,
@@ -30,12 +32,6 @@ import {
ZSurveyThankYouCard,
ZSurveyWelcomeCard,
} from "@formbricks/types/surveys";
import {
TSurvey,
TSurveyLanguage,
TSurveyMultipleChoiceMultiQuestion,
TSurveyQuestion,
} from "@formbricks/types/surveys";
import { structuredClone } from "../pollyfills/structuredClone";
@@ -207,21 +203,16 @@ export const translateQuestion = (
case "multipleChoiceSingle":
case "multipleChoiceMulti":
(clonedQuestion as TSurveyMultipleChoiceSingleQuestion | TSurveyMultipleChoiceMultiQuestion).choices =
question.choices.map((choice) => {
return translateChoice(choice, languages);
});
if (
typeof (clonedQuestion as TSurveyMultipleChoiceSingleQuestion | TSurveyMultipleChoiceMultiQuestion)
.otherOptionPlaceholder !== "undefined"
) {
(
clonedQuestion as TSurveyMultipleChoiceSingleQuestion | TSurveyMultipleChoiceMultiQuestion
).otherOptionPlaceholder = createI18nString(question.otherOptionPlaceholder ?? "", languages);
(clonedQuestion as TSurveyMultipleChoiceQuestion).choices = question.choices.map((choice) => {
return translateChoice(choice, languages);
});
if (typeof (clonedQuestion as TSurveyMultipleChoiceQuestion).otherOptionPlaceholder !== "undefined") {
(clonedQuestion as TSurveyMultipleChoiceQuestion).otherOptionPlaceholder = createI18nString(
question.otherOptionPlaceholder ?? "",
languages
);
}
if (question.type === "multipleChoiceSingle") {
return ZSurveyMultipleChoiceSingleQuestion.parse(clonedQuestion);
} else return ZSurveyMultipleChoiceMultiQuestion.parse(clonedQuestion);
return ZSurveyMultipleChoiceQuestion.parse(clonedQuestion);
case "cta":
if (typeof question.dismissButtonLabel !== "undefined") {

View File

@@ -6,8 +6,7 @@ import { TResponse, TResponseFilterCriteria, TResponseTtc } from "@formbricks/ty
import {
TSurvey,
TSurveyLanguage,
TSurveyMultipleChoiceMultiQuestion,
TSurveyMultipleChoiceSingleQuestion,
TSurveyMultipleChoiceQuestion,
TSurveyQuestionSummaryAddress,
TSurveyQuestionSummaryDate,
TSurveyQuestionSummaryFileUpload,
@@ -678,9 +677,9 @@ const checkForI18n = (response: TResponse, id: string, survey: TSurvey, language
}
// Return the localized value of the choice fo multiSelect single question
const choice = (
question as TSurveyMultipleChoiceMultiQuestion | TSurveyMultipleChoiceSingleQuestion
)?.choices.find((choice) => choice.label[languageCode] === response.data[id]);
const choice = (question as TSurveyMultipleChoiceQuestion)?.choices.find(
(choice) => choice.label[languageCode] === response.data[id]
);
return getLocalizedValue(choice?.label, "default") || response.data[id];
};

View File

@@ -140,7 +140,6 @@ const handleTriggerUpdates = (
currentTriggers: TSurvey["triggers"],
actionClasses: TActionClass[]
) => {
console.log("updatedTriggers", updatedTriggers, currentTriggers);
if (!updatedTriggers) return {};
checkTriggersValidity(updatedTriggers, actionClasses);

View File

@@ -10,10 +10,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyMultipleChoiceMultiQuestion } from "@formbricks/types/surveys";
import type { TSurveyMultipleChoiceQuestion } from "@formbricks/types/surveys";
interface MultipleChoiceMultiProps {
question: TSurveyMultipleChoiceMultiQuestion;
question: TSurveyMultipleChoiceQuestion;
value: string[];
onChange: (responseData: TResponseData) => void;
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;

View File

@@ -10,10 +10,10 @@ import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyMultipleChoiceSingleQuestion } from "@formbricks/types/surveys";
import type { TSurveyMultipleChoiceQuestion } from "@formbricks/types/surveys";
interface MultipleChoiceSingleProps {
question: TSurveyMultipleChoiceSingleQuestion;
question: TSurveyMultipleChoiceQuestion;
value?: string;
onChange: (responseData: TResponseData) => void;
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;

View File

@@ -8,8 +8,7 @@ import {
ZSurveyCalLogic,
ZSurveyConsentLogic,
ZSurveyFileUploadLogic,
ZSurveyMultipleChoiceMultiLogic,
ZSurveyMultipleChoiceSingleLogic,
ZSurveyMultipleChoiceLogic,
ZSurveyNPSLogic,
ZSurveyOpenTextLogic,
ZSurveyOpenTextQuestionInputType,
@@ -56,7 +55,7 @@ export type TLegacySurveyChoice = z.infer<typeof ZLegacySurveyChoice>;
export const ZLegacySurveyMultipleChoiceSingleQuestion = ZLegacySurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionType.MultipleChoiceSingle),
choices: z.array(ZLegacySurveyChoice),
logic: z.array(ZSurveyMultipleChoiceSingleLogic).optional(),
logic: z.array(ZSurveyMultipleChoiceLogic).optional(),
shuffleOption: z.enum(["none", "all", "exceptLast"]).optional(),
otherOptionPlaceholder: z.string().optional(),
});
@@ -68,7 +67,7 @@ export type TLegacySurveyMultipleChoiceSingleQuestion = z.infer<
export const ZLegacySurveyMultipleChoiceMultiQuestion = ZLegacySurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionType.MultipleChoiceMulti),
choices: z.array(ZLegacySurveyChoice),
logic: z.array(ZSurveyMultipleChoiceMultiLogic).optional(),
logic: z.array(ZSurveyMultipleChoiceLogic).optional(),
shuffleOption: z.enum(["none", "all", "exceptLast"]).optional(),
otherOptionPlaceholder: z.string().optional(),
});

View File

@@ -177,13 +177,8 @@ export const ZSurveyConsentLogic = ZSurveyLogicBase.extend({
value: z.undefined(),
});
export const ZSurveyMultipleChoiceSingleLogic = ZSurveyLogicBase.extend({
condition: z.enum(["submitted", "skipped", "equals", "notEquals", "includesOne"]).optional(),
value: z.union([z.array(z.string()), z.string()]).optional(),
});
export const ZSurveyMultipleChoiceMultiLogic = ZSurveyLogicBase.extend({
condition: z.enum(["submitted", "skipped", "includesAll", "includesOne", "equals"]).optional(),
export const ZSurveyMultipleChoiceLogic = ZSurveyLogicBase.extend({
condition: z.enum(["submitted", "skipped", "equals", "notEquals", "includesOne", "includesAll"]).optional(),
value: z.union([z.array(z.string()), z.string()]).optional(),
});
@@ -243,8 +238,7 @@ const ZSurveyMatrixLogic = ZSurveyLogicBase.extend({
export const ZSurveyLogic = z.union([
ZSurveyOpenTextLogic,
ZSurveyConsentLogic,
ZSurveyMultipleChoiceSingleLogic,
ZSurveyMultipleChoiceMultiLogic,
ZSurveyMultipleChoiceLogic,
ZSurveyNPSLogic,
ZSurveyCTALogic,
ZSurveyRatingLogic,
@@ -296,29 +290,38 @@ export const ZSurveyConsentQuestion = ZSurveyQuestionBase.extend({
export type TSurveyConsentQuestion = z.infer<typeof ZSurveyConsentQuestion>;
export const ZSurveyMultipleChoiceSingleQuestion = ZSurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionType.MultipleChoiceSingle),
choices: z.array(ZSurveyChoice),
logic: z.array(ZSurveyMultipleChoiceSingleLogic).optional(),
shuffleOption: z.enum(["none", "all", "exceptLast"]).optional(),
otherOptionPlaceholder: ZI18nString.optional(),
});
export type TSurveyMultipleChoiceSingleQuestion = z.infer<typeof ZSurveyMultipleChoiceSingleQuestion>;
export const ZShuffleOption = z.enum(["none", "all", "exceptLast"]);
export type TShuffleOption = z.infer<typeof ZShuffleOption>;
export const ZSurveyMultipleChoiceMultiQuestion = ZSurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionType.MultipleChoiceMulti),
export const ZSurveyMultipleChoiceQuestion = ZSurveyQuestionBase.extend({
type: z.union([
z.literal(TSurveyQuestionType.MultipleChoiceSingle),
z.literal(TSurveyQuestionType.MultipleChoiceMulti),
]),
choices: z.array(ZSurveyChoice),
logic: z.array(ZSurveyMultipleChoiceMultiLogic).optional(),
logic: z.array(ZSurveyMultipleChoiceLogic).optional(),
shuffleOption: ZShuffleOption.optional(),
otherOptionPlaceholder: ZI18nString.optional(),
});
}).refine(
(question) => {
const { logic, type } = question;
export type TSurveyMultipleChoiceMultiQuestion = z.infer<typeof ZSurveyMultipleChoiceMultiQuestion>;
if (type === TSurveyQuestionType.MultipleChoiceSingle) {
// The single choice question should not have 'includesAll' logic
return !logic?.some((l) => l.condition === "includesAll");
} else {
// The multi choice question should not have 'notEquals' logic
return !logic?.some((l) => l.condition === "notEquals");
}
},
{
message:
"MultipleChoiceSingle question should not have 'includesAll' logic and MultipleChoiceMulti question should not have 'notEquals' logic",
}
);
export type TSurveyMultipleChoiceQuestion = z.infer<typeof ZSurveyMultipleChoiceQuestion>;
export const ZSurveyNPSQuestion = ZSurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionType.NPS),
@@ -409,8 +412,7 @@ export type TSurveyAddressQuestion = z.infer<typeof ZSurveyAddressQuestion>;
export const ZSurveyQuestion = z.union([
ZSurveyOpenTextQuestion,
ZSurveyConsentQuestion,
ZSurveyMultipleChoiceSingleQuestion,
ZSurveyMultipleChoiceMultiQuestion,
ZSurveyMultipleChoiceQuestion,
ZSurveyNPSQuestion,
ZSurveyCTAQuestion,
ZSurveyRatingQuestion,
@@ -557,7 +559,7 @@ export type TSurveyQuestionSummaryOpenText = z.infer<typeof ZSurveyQuestionSumma
export const ZSurveyQuestionSummaryMultipleChoice = z.object({
type: z.union([z.literal("multipleChoiceMulti"), z.literal("multipleChoiceSingle")]),
question: z.union([ZSurveyMultipleChoiceSingleQuestion, ZSurveyMultipleChoiceMultiQuestion]),
question: ZSurveyMultipleChoiceQuestion,
responseCount: z.number(),
choices: z.array(
z.object({

View File

@@ -3,8 +3,7 @@ import {
TI18nString,
TSurvey,
TSurveyMatrixQuestion,
TSurveyMultipleChoiceMultiQuestion,
TSurveyMultipleChoiceSingleQuestion,
TSurveyMultipleChoiceQuestion,
TSurveyQuestion,
} from "@formbricks/types/surveys";
@@ -24,7 +23,7 @@ export const getChoiceLabel = (
choiceIdx: number,
surveyLanguageCodes: string[]
): TI18nString => {
const choiceQuestion = question as TSurveyMultipleChoiceMultiQuestion | TSurveyMultipleChoiceSingleQuestion;
const choiceQuestion = question as TSurveyMultipleChoiceQuestion;
return choiceQuestion.choices[choiceIdx]?.label || createI18nString("", surveyLanguageCodes);
};