diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionCard.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionCard.tsx
index a0c6e7f484..6524a74430 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionCard.tsx
+++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionCard.tsx
@@ -1,27 +1,10 @@
"use client";
-import { getTSurveyQuestionTypeName } from "@/app/lib/questions";
+import { QUESTIONS_ICON_MAP, getTSurveyQuestionTypeName } from "@/app/lib/questions";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import * as Collapsible from "@radix-ui/react-collapsible";
-import {
- ArrowUpFromLineIcon,
- CalendarDaysIcon,
- CheckIcon,
- ChevronDownIcon,
- ChevronRightIcon,
- Grid3X3Icon,
- GripIcon,
- HomeIcon,
- ImageIcon,
- ListIcon,
- MessageSquareTextIcon,
- MousePointerClickIcon,
- PhoneIcon,
- PresentationIcon,
- Rows3Icon,
- StarIcon,
-} from "lucide-react";
+import { ChevronDownIcon, ChevronRightIcon, GripIcon } from "lucide-react";
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
@@ -45,7 +28,7 @@ import { MultipleChoiceQuestionForm } from "./MultipleChoiceQuestionForm";
import { NPSQuestionForm } from "./NPSQuestionForm";
import { OpenQuestionForm } from "./OpenQuestionForm";
import { PictureSelectionForm } from "./PictureSelectionForm";
-import { QuestionDropdown } from "./QuestionMenu";
+import { QuestionMenu } from "./QuestionMenu";
import { RatingQuestionForm } from "./RatingQuestionForm";
interface QuestionCardProps {
@@ -64,6 +47,7 @@ interface QuestionCardProps {
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
+ addQuestion: (question: any, index?: number) => void;
}
export const QuestionCard = ({
@@ -82,6 +66,7 @@ export const QuestionCard = ({
setSelectedLanguageCode,
isInvalid,
attributeClasses,
+ addQuestion,
}: QuestionCardProps) => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: question.id,
@@ -154,7 +139,8 @@ export const QuestionCard = ({
"flex flex-row rounded-lg bg-white transition-all duration-300 ease-in-out"
)}
ref={setNodeRef}
- style={style}>
+ style={style}
+ id={question.id}>
- {question.type === TSurveyQuestionType.FileUpload ? (
-
- ) : question.type === TSurveyQuestionType.OpenText ? (
-
- ) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
-
- ) : question.type === TSurveyQuestionType.MultipleChoiceMulti ? (
-
- ) : question.type === TSurveyQuestionType.NPS ? (
-
- ) : question.type === TSurveyQuestionType.CTA ? (
-
- ) : question.type === TSurveyQuestionType.Rating ? (
-
- ) : question.type === TSurveyQuestionType.Consent ? (
-
- ) : question.type === TSurveyQuestionType.PictureSelection ? (
-
- ) : question.type === TSurveyQuestionType.Date ? (
-
- ) : question.type === TSurveyQuestionType.Cal ? (
-
- ) : question.type === TSurveyQuestionType.Matrix ? (
-
- ) : question.type === TSurveyQuestionType.Address ? (
-
- ) : null}
+ {QUESTIONS_ICON_MAP[question.type]}
@@ -241,12 +201,16 @@ export const QuestionCard = ({
-
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionMenu.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionMenu.tsx
index bf0dfb922f..17cbff84d7 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionMenu.tsx
+++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionMenu.tsx
@@ -1,6 +1,22 @@
"use client";
-import { ArrowDownIcon, ArrowUpIcon, CopyIcon, TrashIcon } from "lucide-react";
+import { QUESTIONS_ICON_MAP, QUESTIONS_NAME_MAP, getQuestionDefaults } from "@/app/lib/questions";
+import { createId } from "@paralleldrive/cuid2";
+import { ArrowDownIcon, ArrowUpIcon, CopyIcon, EllipsisIcon, TrashIcon } from "lucide-react";
+import React, { useState } from "react";
+
+import { TProduct } from "@formbricks/types/product";
+import { TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys";
+import { ConfirmationModal } from "@formbricks/ui/ConfirmationModal";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuTrigger,
+} from "@formbricks/ui/DropdownMenu";
interface QuestionDropdownProps {
questionIdx: number;
@@ -8,39 +24,87 @@ interface QuestionDropdownProps {
duplicateQuestion: (questionIdx: number) => void;
deleteQuestion: (questionIdx: number) => void;
moveQuestion: (questionIdx: number, up: boolean) => void;
+ question: TSurveyQuestion;
+ product: TProduct;
+ updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
+ addQuestion: (question: any, index?: number) => void;
}
-export const QuestionDropdown = ({
+export const QuestionMenu = ({
questionIdx,
lastQuestion,
duplicateQuestion,
deleteQuestion,
moveQuestion,
+ product,
+ question,
+ updateQuestion,
+ addQuestion,
}: QuestionDropdownProps) => {
+ const [logicWarningModal, setLogicWarningModal] = useState(false);
+ const [changeToType, setChangeToType] = useState(question.type);
+
+ const changeQuestionType = (type: TSurveyQuestionType) => {
+ const { headline, required, subheader, imageUrl, videoUrl, buttonLabel, backButtonLabel } = question;
+
+ const questionDefaults = getQuestionDefaults(type, product);
+
+ // if going from single select to multi select or vice versa, we need to keep the choices as well
+
+ if (
+ (type === TSurveyQuestionType.MultipleChoiceSingle &&
+ question.type === TSurveyQuestionType.MultipleChoiceMulti) ||
+ (type === TSurveyQuestionType.MultipleChoiceMulti &&
+ question.type === TSurveyQuestionType.MultipleChoiceSingle)
+ ) {
+ updateQuestion(questionIdx, {
+ choices: question.choices,
+ type,
+ logic: undefined,
+ });
+
+ return;
+ }
+
+ updateQuestion(questionIdx, {
+ ...questionDefaults,
+ type,
+ headline,
+ subheader,
+ required,
+ imageUrl,
+ videoUrl,
+ buttonLabel,
+ backButtonLabel,
+ logic: undefined,
+ });
+ };
+
+ const addQuestionBelow = (type: TSurveyQuestionType) => {
+ const questionDefaults = getQuestionDefaults(type, product);
+
+ addQuestion(
+ {
+ ...questionDefaults,
+ type,
+ id: createId(),
+ required: true,
+ },
+ questionIdx + 1
+ );
+
+ // scroll to the new question
+ const section = document.getElementById(`${question.id}`);
+ section?.scrollIntoView({ behavior: "smooth", block: "end", inline: "end" });
+ };
+
+ const onConfirm = () => {
+ changeQuestionType(changeToType);
+ setLogicWarningModal(false);
+ };
+
return (
-
{
- if (questionIdx !== 0) {
- e.stopPropagation();
- moveQuestion(questionIdx, true);
- }
- }}
- />
- {
- if (!lastQuestion) {
- e.stopPropagation();
- moveQuestion(questionIdx, false);
- }
- }}
- />
{
@@ -55,6 +119,114 @@ export const QuestionDropdown = ({
deleteQuestion(questionIdx);
}}
/>
+
+
+
+
+
+
+
+
+
+
+
+ Change question type
+
+
+
+
+ {Object.entries(QUESTIONS_NAME_MAP).map(([type, name]) => {
+ if (type === question.type) return null;
+
+ return (
+ {
+ setChangeToType(type as TSurveyQuestionType);
+ if (question.logic) {
+ setLogicWarningModal(true);
+ return;
+ }
+
+ changeQuestionType(type as TSurveyQuestionType);
+ }}>
+ {QUESTIONS_ICON_MAP[type as TSurveyQuestionType]}
+ {name}
+
+ );
+ })}
+
+
+
+
+
+
+ Add question below
+
+
+
+
+ {Object.entries(QUESTIONS_NAME_MAP).map(([type, name]) => {
+ if (type === question.type) return null;
+
+ return (
+ {
+ e.stopPropagation();
+ addQuestionBelow(type as TSurveyQuestionType);
+ }}>
+ {QUESTIONS_ICON_MAP[type as TSurveyQuestionType]}
+ {name}
+
+ );
+ })}
+
+
+
{
+ if (questionIdx !== 0) {
+ e.stopPropagation();
+ moveQuestion(questionIdx, true);
+ }
+ }}
+ disabled={questionIdx === 0}>
+ Move up
+
+
+
+
{
+ if (!lastQuestion) {
+ e.stopPropagation();
+ moveQuestion(questionIdx, false);
+ }
+ }}
+ disabled={lastQuestion}>
+ Move down
+
+
+
+
+
+
+
);
};
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsDroppable.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsDroppable.tsx
index 47a3a3310e..56834cf58e 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsDroppable.tsx
+++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsDroppable.tsx
@@ -20,6 +20,7 @@ interface QuestionsDraggableProps {
invalidQuestions: string[] | null;
internalQuestionIdMap: Record
;
attributeClasses: TAttributeClass[];
+ addQuestion: (question: any, index?: number) => void;
}
export const QuestionsDroppable = ({
@@ -36,6 +37,7 @@ export const QuestionsDroppable = ({
updateQuestion,
internalQuestionIdMap,
attributeClasses,
+ addQuestion,
}: QuestionsDraggableProps) => {
return (
@@ -58,6 +60,7 @@ export const QuestionsDroppable = ({
lastQuestion={questionIdx === localSurvey.questions.length - 1}
isInvalid={invalidQuestions ? invalidQuestions.includes(question.id) : false}
attributeClasses={attributeClasses}
+ addQuestion={addQuestion}
/>
))}
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx
index ffb6aa9be5..15e2f1d188 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx
+++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx
@@ -216,14 +216,19 @@ export const QuestionsView = ({
toast.success("Question duplicated.");
};
- const addQuestion = (question: any) => {
+ const addQuestion = (question: any, index?: number) => {
const updatedSurvey = { ...localSurvey };
if (backButtonLabel) {
question.backButtonLabel = backButtonLabel;
}
const languageSymbols = extractLanguageCodes(localSurvey.languages);
const translatedQuestion = translateQuestion(question, languageSymbols);
- updatedSurvey.questions.push({ ...translatedQuestion, isDraft: true });
+
+ if (index) {
+ updatedSurvey.questions.splice(index, 0, { ...translatedQuestion, isDraft: true });
+ } else {
+ updatedSurvey.questions.push({ ...translatedQuestion, isDraft: true });
+ }
setLocalSurvey(updatedSurvey);
setActiveQuestionId(question.id);
@@ -361,6 +366,7 @@ export const QuestionsView = ({
invalidQuestions={invalidQuestions}
internalQuestionIdMap={internalQuestionIdMap}
attributeClasses={attributeClasses}
+ addQuestion={addQuestion}
/>
diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyEditor.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyEditor.tsx
index 421af51fd0..18045dfff5 100644
--- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyEditor.tsx
+++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyEditor.tsx
@@ -143,7 +143,7 @@ export const SurveyEditor = ({
setSelectedLanguageCode={setSelectedLanguageCode}
/>
-
+
({
+ ...prev,
+ [curr.id]: ,
+ }),
+ {}
+);
+
+export const QUESTIONS_NAME_MAP = questionTypes.reduce(
+ (prev, curr) => ({
+ ...prev,
+ [curr.id]: curr.label,
+ }),
+ {}
+) as Record;
+
export const universalQuestionPresets = {
required: true,
};