feat: replaces dnd with dnd-kit (#2564)

Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
This commit is contained in:
Anshuman Pandey
2024-05-14 17:20:53 +05:30
committed by GitHub
parent ba0b68cbfb
commit bc99f15094
11 changed files with 1293 additions and 1226 deletions

View File

@@ -1,9 +1,10 @@
"use client";
import { DndContext } from "@dnd-kit/core";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
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 } from "@formbricks/lib/i18n/utils";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
@@ -19,7 +20,7 @@ import { Label } from "@formbricks/ui/Label";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
import { isLabelValidForAllLanguages } from "../lib/validation";
import { SelectQuestionChoice } from "./SelectQuestionChoice";
interface OpenQuestionFormProps {
localSurvey: TSurvey;
@@ -237,74 +238,56 @@ export const MultipleChoiceQuestionForm = ({
<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 className="inline-flex w-full items-center">
<div className="flex w-full space-x-2">
<QuestionFormInput
key={choice.id}
id={`choice-${choiceIdx}`}
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
localSurvey={localSurvey}
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, surveyLanguages)
}
className={`${choice.id === "other" ? "border border-dashed" : ""}`}
/>
{choice.id === "other" && (
<QuestionFormInput
id="otherOptionPlaceholder"
localSurvey={localSurvey}
placeholder={"Please specify"}
<div className="mt-2" id="choices">
<DndContext
onDragEnd={(event) => {
const { active, over } = event;
if (active.id === "other" || over?.id === "other") {
return;
}
if (!active || !over) {
return;
}
const activeIndex = question.choices.findIndex((choice) => choice.id === active.id);
const overIndex = question.choices.findIndex((choice) => choice.id === over.id);
const newChoices = [...question.choices];
newChoices.splice(activeIndex, 1);
newChoices.splice(overIndex, 0, question.choices[activeIndex]);
updateQuestion(questionIdx, { choices: newChoices });
}}>
<SortableContext items={question.choices} strategy={verticalListSortingStrategy}>
<div className="flex flex-col">
{question.choices &&
question.choices.map((choice, choiceIdx) => (
<SelectQuestionChoice
key={choice.id}
choice={choice}
choiceIdx={choiceIdx}
questionIdx={questionIdx}
value={
question.otherOptionPlaceholder
? question.otherOptionPlaceholder
: createI18nString("Please specify", surveyLanguageCodes)
}
updateQuestion={updateQuestion}
updateChoice={updateChoice}
deleteChoice={deleteChoice}
addChoice={addChoice}
setisInvalidValue={setisInvalidValue}
isInvalid={isInvalid}
localSurvey={localSurvey}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={
isInvalid &&
!isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguages)
}
className="border border-dashed"
surveyLanguages={surveyLanguages}
findDuplicateLabel={findDuplicateLabel}
question={question}
updateQuestion={updateQuestion}
surveyLanguageCodes={surveyLanguageCodes}
/>
)}
</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>
))}
</SortableContext>
</DndContext>
<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()}>

View File

@@ -1,6 +1,8 @@
"use client";
import { 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,
@@ -9,6 +11,7 @@ import {
ChevronDownIcon,
ChevronRightIcon,
Grid3X3Icon,
GripIcon,
HomeIcon,
ImageIcon,
ListIcon,
@@ -20,12 +23,11 @@ import {
StarIcon,
} from "lucide-react";
import { useState } from "react";
import { Draggable } from "react-beautiful-dnd";
import { cn } from "@formbricks/lib/cn";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TProduct } from "@formbricks/types/product";
import { TI18nString, TSurvey, TSurveyQuestionType } from "@formbricks/types/surveys";
import { TI18nString, TSurvey, TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys";
import { Label } from "@formbricks/ui/Label";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
import { Switch } from "@formbricks/ui/Switch";
@@ -48,6 +50,7 @@ import { RatingQuestionForm } from "./RatingQuestionForm";
interface QuestionCardProps {
localSurvey: TSurvey;
product: TProduct;
question: TSurveyQuestion;
questionIdx: number;
moveQuestion: (questionIndex: number, up: boolean) => void;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
@@ -64,6 +67,7 @@ interface QuestionCardProps {
export default function QuestionCard({
localSurvey,
product,
question,
questionIdx,
moveQuestion,
updateQuestion,
@@ -76,7 +80,10 @@ export default function QuestionCard({
setSelectedLanguageCode,
isInvalid,
}: QuestionCardProps) {
const question = localSurvey.questions[questionIdx];
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: question.id,
});
const open = activeQuestionId === question.id;
const [openAdvanced, setOpenAdvanced] = useState(question.logic && question.logic.length > 0);
@@ -131,369 +138,374 @@ export default function QuestionCard({
}
};
const style = {
transition: transition ?? "transform 100ms ease",
transform: CSS.Translate.toString(transform),
zIndex: isDragging ? 10 : 1,
};
return (
<Draggable draggableId={question.id} index={questionIdx}>
{(provided) => (
<div
className={cn(
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
"flex flex-row rounded-lg bg-white transition-all duration-300 ease-in-out"
)}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}>
<div
className={cn(
open ? "bg-slate-700" : "bg-slate-400",
"top-0 w-10 rounded-l-lg p-2 text-center text-sm text-white hover:cursor-grab hover:bg-slate-600",
isInvalid && "bg-red-400 hover:bg-red-600"
)}>
{questionIdx + 1}
</div>
<Collapsible.Root
open={open}
onOpenChange={() => {
if (activeQuestionId !== question.id) {
setActiveQuestionId(question.id);
} else {
setActiveQuestionId(null);
}
}}
className="flex-1 rounded-r-lg border border-slate-200">
<Collapsible.CollapsibleTrigger
asChild
className={cn(open ? "" : " ", "flex cursor-pointer justify-between p-4 hover:bg-slate-50")}>
<div
className={cn(
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
"flex flex-row rounded-lg bg-white transition-all duration-300 ease-in-out"
)}
ref={setNodeRef}
style={style}>
<div
{...listeners}
{...attributes}
className={cn(
open ? "bg-slate-700" : "bg-slate-400",
"top-0 w-10 rounded-l-lg p-2 text-center text-sm text-white hover:cursor-grab hover:bg-slate-600",
isInvalid && "bg-red-400 hover:bg-red-600",
"flex flex-col items-center justify-between"
)}>
<span>{questionIdx + 1}</span>
<button className="hidden hover:cursor-move group-hover:block">
<GripIcon className="h-4 w-4" />
</button>
</div>
<Collapsible.Root
open={open}
onOpenChange={() => {
if (activeQuestionId !== question.id) {
setActiveQuestionId(question.id);
} else {
setActiveQuestionId(null);
}
}}
className="flex-1 rounded-r-lg border border-slate-200">
<Collapsible.CollapsibleTrigger
asChild
className={cn(open ? "" : " ", "flex cursor-pointer justify-between p-4 hover:bg-slate-50")}>
<div>
<div className="inline-flex">
<div className="-ml-0.5 mr-3 h-6 min-w-[1.5rem] text-slate-400">
{question.type === TSurveyQuestionType.FileUpload ? (
<ArrowUpFromLineIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.OpenText ? (
<MessageSquareTextIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
<Rows3Icon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.MultipleChoiceMulti ? (
<ListIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.NPS ? (
<PresentationIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.CTA ? (
<MousePointerClickIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.Rating ? (
<StarIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.Consent ? (
<CheckIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.PictureSelection ? (
<ImageIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.Date ? (
<CalendarDaysIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.Cal ? (
<PhoneIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.Matrix ? (
<Grid3X3Icon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.Address ? (
<HomeIcon className="h-5 w-5" />
) : null}
</div>
<div>
<div className="inline-flex">
<div className="-ml-0.5 mr-3 h-6 min-w-[1.5rem] text-slate-400">
{question.type === TSurveyQuestionType.FileUpload ? (
<ArrowUpFromLineIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.OpenText ? (
<MessageSquareTextIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
<Rows3Icon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.MultipleChoiceMulti ? (
<ListIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.NPS ? (
<PresentationIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.CTA ? (
<MousePointerClickIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.Rating ? (
<StarIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.Consent ? (
<CheckIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.PictureSelection ? (
<ImageIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.Date ? (
<CalendarDaysIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.Cal ? (
<PhoneIcon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.Matrix ? (
<Grid3X3Icon className="h-5 w-5" />
) : question.type === TSurveyQuestionType.Address ? (
<HomeIcon className="h-5 w-5" />
) : null}
</div>
<div>
<p className="text-sm font-semibold">
{recallToHeadline(question.headline, localSurvey, true, selectedLanguageCode)[
selectedLanguageCode
]
? formatTextWithSlashes(
recallToHeadline(question.headline, localSurvey, true, selectedLanguageCode)[
selectedLanguageCode
] ?? ""
)
: getTSurveyQuestionTypeName(question.type)}
</p>
{!open && question?.required && (
<p className="mt-1 truncate text-xs text-slate-500">
{question?.required && "Required"}
</p>
)}
</div>
</div>
<div className="flex items-center space-x-2">
<QuestionDropdown
questionIdx={questionIdx}
lastQuestion={lastQuestion}
duplicateQuestion={duplicateQuestion}
deleteQuestion={deleteQuestion}
moveQuestion={moveQuestion}
/>
</div>
</div>
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="px-4 pb-4">
{question.type === TSurveyQuestionType.OpenText ? (
<OpenQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
<MultipleChoiceQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.MultipleChoiceMulti ? (
<MultipleChoiceQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.NPS ? (
<NPSQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.CTA ? (
<CTAQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.Rating ? (
<RatingQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.Consent ? (
<ConsentQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.Date ? (
<DateQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.PictureSelection ? (
<PictureSelectionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.FileUpload ? (
<FileUploadQuestionForm
localSurvey={localSurvey}
product={product}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.Cal ? (
<CalQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.Matrix ? (
<MatrixQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.Address ? (
<AddressQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : null}
<div className="mt-4">
<Collapsible.Root open={openAdvanced} onOpenChange={setOpenAdvanced} className="mt-5">
<Collapsible.CollapsibleTrigger className="flex items-center text-sm text-slate-700">
{openAdvanced ? (
<ChevronDownIcon className="mr-1 h-4 w-3" />
) : (
<ChevronRightIcon className="mr-2 h-4 w-3" />
)}
{openAdvanced ? "Hide Advanced Settings" : "Show Advanced Settings"}
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="space-y-4">
{question.type !== TSurveyQuestionType.NPS &&
question.type !== TSurveyQuestionType.Rating &&
question.type !== TSurveyQuestionType.CTA ? (
<div className="mt-2 flex space-x-2">
<div className="w-full">
<QuestionFormInput
id="buttonLabel"
value={question.buttonLabel}
localSurvey={localSurvey}
questionIdx={questionIdx}
maxLength={48}
placeholder={lastQuestion ? "Finish" : "Next"}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
onBlur={(e) => {
if (!question.buttonLabel) return;
let translatedNextButtonLabel = {
...question.buttonLabel,
[selectedLanguageCode]: e.target.value,
};
if (questionIdx === localSurvey.questions.length - 1) return;
updateEmptyNextButtonLabels(translatedNextButtonLabel);
}}
/>
</div>
{questionIdx !== 0 && (
<QuestionFormInput
id="backButtonLabel"
value={question.backButtonLabel}
localSurvey={localSurvey}
questionIdx={questionIdx}
maxLength={48}
placeholder={"Back"}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
/>
)}
</div>
) : null}
{(question.type === TSurveyQuestionType.Rating ||
question.type === TSurveyQuestionType.NPS) &&
questionIdx !== 0 && (
<div className="mt-4">
<QuestionFormInput
id="backButtonLabel"
value={question.backButtonLabel}
localSurvey={localSurvey}
questionIdx={questionIdx}
maxLength={48}
placeholder={"Back"}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
/>
</div>
)}
<AdvancedSettings
question={question}
questionIdx={questionIdx}
localSurvey={localSurvey}
updateQuestion={updateQuestion}
/>
</Collapsible.CollapsibleContent>
</Collapsible.Root>
</div>
</Collapsible.CollapsibleContent>
{open && (
<div className="mx-4 flex justify-end space-x-6 border-t border-slate-200">
{question.type === "openText" && (
<div className="my-4 flex items-center justify-end space-x-2">
<Label htmlFor="longAnswer">Long Answer</Label>
<Switch
id="longAnswer"
disabled={question.inputType !== "text"}
checked={question.longAnswer !== false}
onClick={(e) => {
e.stopPropagation();
updateQuestion(questionIdx, {
longAnswer:
typeof question.longAnswer === "undefined" ? false : !question.longAnswer,
});
}}
/>
</div>
<p className="text-sm font-semibold">
{recallToHeadline(question.headline, localSurvey, true, selectedLanguageCode)[
selectedLanguageCode
]
? formatTextWithSlashes(
recallToHeadline(question.headline, localSurvey, true, selectedLanguageCode)[
selectedLanguageCode
] ?? ""
)
: getTSurveyQuestionTypeName(question.type)}
</p>
{!open && question?.required && (
<p className="mt-1 truncate text-xs text-slate-500">{question?.required && "Required"}</p>
)}
{
<div className="my-4 flex items-center justify-end space-x-2">
<Label htmlFor="required-toggle">Required</Label>
<Switch
id="required-toggle"
checked={question.required}
disabled={getIsRequiredToggleDisabled()}
onClick={(e) => {
e.stopPropagation();
handleRequiredToggle();
}}
/>
</div>
</div>
<div className="flex items-center space-x-2">
<QuestionDropdown
questionIdx={questionIdx}
lastQuestion={lastQuestion}
duplicateQuestion={duplicateQuestion}
deleteQuestion={deleteQuestion}
moveQuestion={moveQuestion}
/>
</div>
</div>
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="px-4 pb-4">
{question.type === TSurveyQuestionType.OpenText ? (
<OpenQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
<MultipleChoiceQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.MultipleChoiceMulti ? (
<MultipleChoiceQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.NPS ? (
<NPSQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.CTA ? (
<CTAQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.Rating ? (
<RatingQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.Consent ? (
<ConsentQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.Date ? (
<DateQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.PictureSelection ? (
<PictureSelectionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.FileUpload ? (
<FileUploadQuestionForm
localSurvey={localSurvey}
product={product}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.Cal ? (
<CalQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.Matrix ? (
<MatrixQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : question.type === TSurveyQuestionType.Address ? (
<AddressQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
/>
) : null}
<div className="mt-4">
<Collapsible.Root open={openAdvanced} onOpenChange={setOpenAdvanced} className="mt-5">
<Collapsible.CollapsibleTrigger className="flex items-center text-sm text-slate-700">
{openAdvanced ? (
<ChevronDownIcon className="mr-1 h-4 w-3" />
) : (
<ChevronRightIcon className="mr-2 h-4 w-3" />
)}
{openAdvanced ? "Hide Advanced Settings" : "Show Advanced Settings"}
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="space-y-4">
{question.type !== TSurveyQuestionType.NPS &&
question.type !== TSurveyQuestionType.Rating &&
question.type !== TSurveyQuestionType.CTA ? (
<div className="mt-2 flex space-x-2">
<div className="w-full">
<QuestionFormInput
id="buttonLabel"
value={question.buttonLabel}
localSurvey={localSurvey}
questionIdx={questionIdx}
maxLength={48}
placeholder={lastQuestion ? "Finish" : "Next"}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
onBlur={(e) => {
if (!question.buttonLabel) return;
let translatedNextButtonLabel = {
...question.buttonLabel,
[selectedLanguageCode]: e.target.value,
};
if (questionIdx === localSurvey.questions.length - 1) return;
updateEmptyNextButtonLabels(translatedNextButtonLabel);
}}
/>
</div>
{questionIdx !== 0 && (
<QuestionFormInput
id="backButtonLabel"
value={question.backButtonLabel}
localSurvey={localSurvey}
questionIdx={questionIdx}
maxLength={48}
placeholder={"Back"}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
/>
)}
</div>
}
) : null}
{(question.type === TSurveyQuestionType.Rating ||
question.type === TSurveyQuestionType.NPS) &&
questionIdx !== 0 && (
<div className="mt-4">
<QuestionFormInput
id="backButtonLabel"
value={question.backButtonLabel}
localSurvey={localSurvey}
questionIdx={questionIdx}
maxLength={48}
placeholder={"Back"}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
/>
</div>
)}
<AdvancedSettings
question={question}
questionIdx={questionIdx}
localSurvey={localSurvey}
updateQuestion={updateQuestion}
/>
</Collapsible.CollapsibleContent>
</Collapsible.Root>
</div>
</Collapsible.CollapsibleContent>
{open && (
<div className="mx-4 flex justify-end space-x-6 border-t border-slate-200">
{question.type === "openText" && (
<div className="my-4 flex items-center justify-end space-x-2">
<Label htmlFor="longAnswer">Long Answer</Label>
<Switch
id="longAnswer"
disabled={question.inputType !== "text"}
checked={question.longAnswer !== false}
onClick={(e) => {
e.stopPropagation();
updateQuestion(questionIdx, {
longAnswer: typeof question.longAnswer === "undefined" ? false : !question.longAnswer,
});
}}
/>
</div>
)}
</Collapsible.Root>
</div>
)}
</Draggable>
{
<div className="my-4 flex items-center justify-end space-x-2">
<Label htmlFor="required-toggle">Required</Label>
<Switch
id="required-toggle"
checked={question.required}
disabled={getIsRequiredToggleDisabled()}
onClick={(e) => {
e.stopPropagation();
handleRequiredToggle();
}}
/>
</div>
}
</div>
)}
</Collapsible.Root>
</div>
);
}

View File

@@ -0,0 +1,62 @@
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { TProduct } from "@formbricks/types/product";
import { TSurvey } from "@formbricks/types/surveys";
import QuestionCard from "./QuestionCard";
interface QuestionsDraggableProps {
localSurvey: TSurvey;
product: TProduct;
moveQuestion: (questionIndex: number, up: boolean) => void;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
deleteQuestion: (questionIdx: number) => void;
duplicateQuestion: (questionIdx: number) => void;
activeQuestionId: string | null;
setActiveQuestionId: (questionId: string | null) => void;
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
invalidQuestions: string[] | null;
internalQuestionIdMap: Record<string, string>;
}
export const QuestionsDroppable = ({
activeQuestionId,
deleteQuestion,
duplicateQuestion,
invalidQuestions,
localSurvey,
moveQuestion,
product,
selectedLanguageCode,
setActiveQuestionId,
setSelectedLanguageCode,
updateQuestion,
internalQuestionIdMap,
}: QuestionsDraggableProps) => {
return (
<div className="group mb-5 grid w-full gap-5">
<SortableContext items={localSurvey.questions} strategy={verticalListSortingStrategy}>
{localSurvey.questions.map((question, questionIdx) => (
<QuestionCard
key={internalQuestionIdMap[question.id]}
localSurvey={localSurvey}
product={product}
question={question}
questionIdx={questionIdx}
moveQuestion={moveQuestion}
updateQuestion={updateQuestion}
duplicateQuestion={duplicateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
deleteQuestion={deleteQuestion}
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}
lastQuestion={questionIdx === localSurvey.questions.length - 1}
isInvalid={invalidQuestions ? invalidQuestions.includes(question.id) : false}
/>
))}
</SortableContext>
</div>
);
};

View File

@@ -1,8 +1,15 @@
"use client";
import {
DndContext,
DragEndEvent,
PointerSensor,
closestCorners,
useSensor,
useSensors,
} from "@dnd-kit/core";
import { createId } from "@paralleldrive/cuid2";
import { useEffect, useMemo, useState } from "react";
import { DragDropContext } from "react-beautiful-dnd";
import React, { SetStateAction, useEffect, useMemo, useState } from "react";
import toast from "react-hot-toast";
import { MultiLanguageCard } from "@formbricks/ee/multiLanguage/components/MultiLanguageCard";
@@ -17,12 +24,11 @@ import AddQuestionButton from "./AddQuestionButton";
import EditThankYouCard from "./EditThankYouCard";
import EditWelcomeCard from "./EditWelcomeCard";
import HiddenFieldsCard from "./HiddenFieldsCard";
import QuestionCard from "./QuestionCard";
import { StrictModeDroppable } from "./StrictModeDroppable";
import { QuestionsDroppable } from "./QuestionsDroppable";
interface QuestionsViewProps {
localSurvey: TSurvey;
setLocalSurvey: (survey: TSurvey) => void;
setLocalSurvey: React.Dispatch<SetStateAction<TSurvey>>;
activeQuestionId: string | null;
setActiveQuestionId: (questionId: string | null) => void;
product: TProduct;
@@ -53,6 +59,7 @@ export const QuestionsView = ({
return acc;
}, {});
}, [localSurvey.questions]);
const surveyLanguages = localSurvey.languages;
const [backButtonLabel, setbackButtonLabel] = useState(null);
const handleQuestionLogicChange = (survey: TSurvey, compareId: string, updatedId: string): TSurvey => {
@@ -211,17 +218,6 @@ export const QuestionsView = ({
internalQuestionIdMap[question.id] = createId();
};
const onDragEnd = (result) => {
if (!result.destination) {
return;
}
const newQuestions = Array.from(localSurvey.questions);
const [reorderedQuestion] = newQuestions.splice(result.source.index, 1);
newQuestions.splice(result.destination.index, 0, reorderedQuestion);
const updatedSurvey = { ...localSurvey, questions: newQuestions };
setLocalSurvey(updatedSurvey);
};
const moveQuestion = (questionIndex: number, up: boolean) => {
const newQuestions = Array.from(localSurvey.questions);
const [reorderedQuestion] = newQuestions.splice(questionIndex, 1);
@@ -303,6 +299,26 @@ export const QuestionsView = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeQuestionId, setActiveQuestionId]);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 5,
},
})
);
const onDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
const newQuestions = Array.from(localSurvey.questions);
const sourceIndex = newQuestions.findIndex((question) => question.id === active.id);
const destinationIndex = newQuestions.findIndex((question) => question.id === over?.id);
const [reorderedQuestion] = newQuestions.splice(sourceIndex, 1);
newQuestions.splice(destinationIndex, 0, reorderedQuestion);
const updatedSurvey = { ...localSurvey, questions: newQuestions };
setLocalSurvey(updatedSurvey);
};
return (
<div className="mt-16 px-5 py-4">
<div className="mb-5 flex flex-col gap-5">
@@ -316,36 +332,24 @@ export const QuestionsView = ({
selectedLanguageCode={selectedLanguageCode}
/>
</div>
<DragDropContext onDragEnd={onDragEnd}>
<div className="mb-5 grid grid-cols-1 gap-5 ">
<StrictModeDroppable droppableId="questionsList">
{(provided) => (
<div className="grid w-full gap-5" ref={provided.innerRef} {...provided.droppableProps}>
{localSurvey.questions.map((question, questionIdx) => (
// display a question form
<QuestionCard
key={internalQuestionIdMap[question.id]}
localSurvey={localSurvey}
product={product}
questionIdx={questionIdx}
moveQuestion={moveQuestion}
updateQuestion={updateQuestion}
duplicateQuestion={duplicateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
deleteQuestion={deleteQuestion}
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}
lastQuestion={questionIdx === localSurvey.questions.length - 1}
isInvalid={invalidQuestions ? invalidQuestions.includes(question.id) : false}
/>
))}
{provided.placeholder}
</div>
)}
</StrictModeDroppable>
</div>
</DragDropContext>
<DndContext sensors={sensors} onDragEnd={onDragEnd} collisionDetection={closestCorners}>
<QuestionsDroppable
localSurvey={localSurvey}
product={product}
moveQuestion={moveQuestion}
updateQuestion={updateQuestion}
duplicateQuestion={duplicateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
deleteQuestion={deleteQuestion}
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}
invalidQuestions={invalidQuestions}
internalQuestionIdMap={internalQuestionIdMap}
/>
</DndContext>
<AddQuestionButton addQuestion={addQuestion} product={product} />
<div className="mt-5 flex flex-col gap-5">
<EditThankYouCard

View File

@@ -0,0 +1,143 @@
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { GripVerticalIcon, PlusIcon, TrashIcon } from "lucide-react";
import toast from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
import { createI18nString } from "@formbricks/lib/i18n/utils";
import {
TI18nString,
TSurvey,
TSurveyLanguage,
TSurveyMultipleChoiceQuestion,
} from "@formbricks/types/surveys";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
import { isLabelValidForAllLanguages } from "../lib/validation";
interface ChoiceProps {
choice: {
id: string;
label: Record<string, string>;
};
choiceIdx: number;
questionIdx: number;
updateChoice: (choiceIdx: number, updatedAttributes: { label: TI18nString }) => void;
deleteChoice: (choiceIdx: number) => void;
addChoice: (choiceIdx: number) => void;
setisInvalidValue: (value: string | null) => void;
isInvalid: boolean;
localSurvey: TSurvey;
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
surveyLanguages: TSurveyLanguage[];
findDuplicateLabel: () => string | null;
question: TSurveyMultipleChoiceQuestion;
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyMultipleChoiceQuestion>) => void;
surveyLanguageCodes: string[];
}
export const SelectQuestionChoice = ({
addChoice,
choice,
choiceIdx,
deleteChoice,
isInvalid,
localSurvey,
questionIdx,
selectedLanguageCode,
setSelectedLanguageCode,
setisInvalidValue,
surveyLanguages,
updateChoice,
findDuplicateLabel,
question,
surveyLanguageCodes,
updateQuestion,
}: ChoiceProps) => {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
id: choice.id,
disabled: choice.id === "other",
});
const style = {
transition: transition ?? "transform 100ms ease",
transform: CSS.Translate.toString(transform),
};
return (
<div className="flex w-full items-center gap-2" ref={setNodeRef} style={style}>
{/* drag handle */}
<div
className={cn("flex items-center", choice.id === "other" && "invisible")}
{...listeners}
{...attributes}>
<GripVerticalIcon className="mt-3 h-4 w-4 cursor-move text-slate-400" />
</div>
<div className="flex w-full space-x-2">
<QuestionFormInput
key={choice.id}
id={`choice-${choiceIdx}`}
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
localSurvey={localSurvey}
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, surveyLanguages)
}
className={`${choice.id === "other" ? "border border-dashed" : ""} mt-0`}
/>
{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, surveyLanguages)
}
className="border border-dashed"
/>
)}
</div>
<div className="mt-3 flex items-center gap-2">
{question.choices && question.choices.length > 2 && (
<TrashIcon
className="h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
onClick={() => deleteChoice(choiceIdx)}
/>
)}
<div className="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>
);
};

View File

@@ -1,19 +0,0 @@
import { useEffect, useState } from "react";
import { Droppable, DroppableProps } from "react-beautiful-dnd";
// React beautiful DnD currently not working with React 18 Strict Mode
export const StrictModeDroppable = ({ children, ...props }: DroppableProps) => {
const [enabled, setEnabled] = useState(false);
useEffect(() => {
const animation = requestAnimationFrame(() => setEnabled(true));
return () => {
cancelAnimationFrame(animation);
setEnabled(false);
};
}, []);
if (!enabled) {
return null;
}
return <Droppable {...props}>{children}</Droppable>;
};

View File

@@ -12,9 +12,12 @@
"lint": "next lint"
},
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@formbricks/api": "workspace:*",
"@formbricks/database": "workspace:*",
"@formbricks/ee": "workspace:*",
"@formbricks/email": "workspace:*",
"@formbricks/js": "workspace:*",
"@formbricks/js-core": "workspace:*",
"@formbricks/lib": "workspace:*",
@@ -22,7 +25,6 @@
"@formbricks/tailwind-config": "workspace:*",
"@formbricks/types": "workspace:*",
"@formbricks/ui": "workspace:*",
"@formbricks/email": "workspace:*",
"@headlessui/react": "^2.0.3",
"@json2csv/node": "^7.0.6",
"@opentelemetry/auto-instrumentations-node": "^0.46.0",
@@ -55,7 +57,6 @@
"prismjs": "^1.29.0",
"qrcode": "^1.5.3",
"react": "18.3.1",
"react-beautiful-dnd": "^13.1.1",
"react-dom": "18.3.1",
"react-hook-form": "^7.51.4",
"react-hot-toast": "^2.4.1",

View File

@@ -284,7 +284,7 @@ test.describe("Multi Language Survey Create", async () => {
await page.getByLabel("Headline").fill(surveys.germanCreate.welcomeCard.headline);
// Fill Open text question in german
await page.getByRole("button", { name: "Free text Required" }).click();
await page.getByRole("main").getByText("Free text").click();
await page.getByPlaceholder("Your question here. Recall").click();
await page
.getByPlaceholder("Your question here. Recall")
@@ -297,7 +297,7 @@ test.describe("Multi Language Survey Create", async () => {
await page.getByLabel("Placeholder").fill(surveys.germanCreate.openTextQuestion.placeholder);
// Fill Single select question in german
await page.getByRole("button", { name: "Single-Select Required" }).click();
await page.getByRole("main").getByText("Single-Select").click();
await page.getByPlaceholder("Your question here. Recall").click();
await page
.getByPlaceholder("Your question here. Recall")
@@ -312,7 +312,8 @@ test.describe("Multi Language Survey Create", async () => {
await page.getByPlaceholder("Option 2").fill(surveys.germanCreate.singleSelectQuestion.options[1]);
// Fill Multi select question in german
await page.getByRole("button", { name: "Multi-Select Required" }).click();
await page.getByRole("main").getByText("Multi-Select").click();
await page.getByPlaceholder("Your question here. Recall").click();
await page
.getByPlaceholder("Your question here. Recall")
@@ -325,7 +326,7 @@ test.describe("Multi Language Survey Create", async () => {
await page.getByPlaceholder("Option 3").fill(surveys.germanCreate.multiSelectQuestion.options[2]);
// Fill Picture select question in german
await page.getByRole("button", { name: "Picture Selection Required" }).click();
await page.getByRole("main").getByText("Picture Selection").click();
await page.getByPlaceholder("Your question here. Recall").click();
await page
.getByPlaceholder("Your question here. Recall")
@@ -336,7 +337,7 @@ test.describe("Multi Language Survey Create", async () => {
.fill(surveys.germanCreate.pictureSelectQuestion.description);
// Fill Rating question in german
await page.getByRole("button", { name: "Rating Required" }).click();
await page.getByRole("main").getByText("Rating").click();
await page.getByPlaceholder("Your question here. Recall").click();
await page
.getByPlaceholder("Your question here. Recall")
@@ -351,7 +352,7 @@ test.describe("Multi Language Survey Create", async () => {
await page.getByPlaceholder("Very satisfied").fill(surveys.germanCreate.ratingQuestion.highLabel);
// Fill NPS question in german
await page.getByRole("button", { name: "Net Promoter Score (NPS) Required" }).click();
await page.getByRole("main").getByText("Net Promoter Score (NPS)").click();
await page.getByPlaceholder("Your question here. Recall").click();
await page.getByPlaceholder("Your question here. Recall").fill(surveys.germanCreate.npsQuestion.question);
await page.getByLabel("Lower Label").click();
@@ -360,21 +361,21 @@ test.describe("Multi Language Survey Create", async () => {
await page.getByLabel("Upper Label").fill(surveys.germanCreate.npsQuestion.highLabel);
// Fill Date question in german
await page.getByRole("button", { name: "Date Required" }).click();
await page.getByRole("main").getByText("Date").click();
await page.getByPlaceholder("Your question here. Recall").click();
await page
.getByPlaceholder("Your question here. Recall")
.fill(surveys.germanCreate.dateQuestion.question);
// Fill File upload question in german
await page.getByRole("button", { name: "File Upload Required" }).click();
await page.getByRole("main").getByText("File Upload").click();
await page.getByPlaceholder("Your question here. Recall").click();
await page
.getByPlaceholder("Your question here. Recall")
.fill(surveys.germanCreate.fileUploadQuestion.question);
// Fill Matrix question in german
await page.getByRole("button", { name: "9 Matrix" }).click();
await page.getByRole("main").getByText("Matrix").click();
await page.getByPlaceholder("Your question here. Recall").click();
await page.getByPlaceholder("Your question here. Recall").fill(surveys.germanCreate.matrix.question);
await page.getByPlaceholder("Your description here. Recall").click();
@@ -397,7 +398,7 @@ test.describe("Multi Language Survey Create", async () => {
await page.locator("#column-3").fill(surveys.germanCreate.matrix.columns[3]);
// Fill Address question in german
await page.getByRole("button", { name: "Address Required" }).click();
await page.getByRole("main").getByText("Address").click();
await page.getByPlaceholder("Your question here. Recall").click();
await page
.getByPlaceholder("Your question here. Recall")

View File

@@ -145,11 +145,13 @@ export const createSurvey = async (
await page.getByText("Welcome CardOn").click();
// Open Text Question
await page.getByRole("button", { name: "1 What would you like to know" }).click();
await page.getByRole("main").getByText("What would you like to know?").click();
await page.getByLabel("Question").fill(params.openTextQuestion.question);
await page.getByLabel("Description").fill(params.openTextQuestion.description);
await page.getByLabel("Placeholder").fill(params.openTextQuestion.placeholder);
await page.getByRole("button", { name: params.openTextQuestion.question }).click();
await page.locator("p").filter({ hasText: params.openTextQuestion.question }).click();
// Single Select Question
await page

View File

@@ -382,9 +382,10 @@ export const QuestionFormInput = ({
<div className="w-full">
<div className="w-full">
<div className="mb-2 mt-3">
<Label htmlFor={id}>{label ?? getLabelById(id)}</Label>
<Label htmlFor={id}>{getLabelById(id)}</Label>
</div>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4 bg-white">
{showImageUploader && id === "headline" && (
<FileInput
id="question-image"

1315
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff