mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-21 18:18:48 -06:00
fix: multi and single select questions (#2606)
This commit is contained in:
@@ -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 "Other"
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -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">
|
||||
@@ -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}
|
||||
|
||||
@@ -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[]) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
@@ -140,7 +140,6 @@ const handleTriggerUpdates = (
|
||||
currentTriggers: TSurvey["triggers"],
|
||||
actionClasses: TActionClass[]
|
||||
) => {
|
||||
console.log("updatedTriggers", updatedTriggers, currentTriggers);
|
||||
if (!updatedTriggers) return {};
|
||||
checkTriggersValidity(updatedTriggers, actionClasses);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user