mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-20 11:22:55 -05:00
Merge branch 'feat/blocks-ui-2' into fix/blocks-migration
This commit is contained in:
@@ -36,6 +36,7 @@ import { PictureSelectionForm } from "@/modules/survey/editor/components/picture
|
||||
import { RankingQuestionForm } from "@/modules/survey/editor/components/ranking-question-form";
|
||||
import { RatingQuestionForm } from "@/modules/survey/editor/components/rating-question-form";
|
||||
import { formatTextWithSlashes } from "@/modules/survey/editor/lib/utils";
|
||||
import { isLabelValidForAllLanguages } from "@/modules/survey/editor/lib/validation";
|
||||
import { getQuestionIconMap, getTSurveyQuestionTypeEnumName } from "@/modules/survey/lib/questions";
|
||||
import { Alert, AlertButton, AlertTitle } from "@/modules/ui/components/alert";
|
||||
|
||||
@@ -46,8 +47,8 @@ interface BlockCardProps {
|
||||
blockIdx: number;
|
||||
moveQuestion: (questionIndex: number, up: boolean) => void;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
updateBlockLogic: (blockIdx: number, logic: TSurveyBlockLogic[]) => void;
|
||||
updateBlockLogicFallback: (blockIdx: number, logicFallback: string | undefined) => void;
|
||||
updateBlockLogic: (questionIdx: number, logic: TSurveyBlockLogic[]) => void;
|
||||
updateBlockLogicFallback: (questionIdx: number, logicFallback: string | undefined) => void;
|
||||
updateBlockButtonLabel: (
|
||||
blockIndex: number,
|
||||
labelKey: "buttonLabel" | "backButtonLabel",
|
||||
@@ -128,11 +129,293 @@ export const BlockCard = ({
|
||||
|
||||
const hasInvalidElement = block.elements.some((element) => invalidQuestions?.includes(element.id));
|
||||
|
||||
const [openAdvanced, setOpenAdvanced] = useState(blockLogic.length > 0);
|
||||
// Check if button labels have incomplete translations for any enabled language
|
||||
// A button label is invalid if it exists but doesn't have valid text for all enabled languages
|
||||
const hasInvalidButtonLabel =
|
||||
block.buttonLabel !== undefined &&
|
||||
!isLabelValidForAllLanguages(block.buttonLabel, localSurvey.languages ?? []);
|
||||
|
||||
// Check if back button label is invalid
|
||||
// Back button label should exist for all blocks except the first one
|
||||
const hasInvalidBackButtonLabel =
|
||||
blockIdx > 0 &&
|
||||
block.backButtonLabel !== undefined &&
|
||||
!isLabelValidForAllLanguages(block.backButtonLabel, localSurvey.languages ?? []);
|
||||
|
||||
// Block should be highlighted if it has invalid elements OR invalid button labels
|
||||
const isBlockInvalid = hasInvalidElement || hasInvalidButtonLabel || hasInvalidBackButtonLabel;
|
||||
|
||||
const [isBlockCollapsed, setIsBlockCollapsed] = useState(false);
|
||||
const [openAdvanced, setOpenAdvanced] = useState(blockLogic.length > 0);
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
const [elementsParent] = useAutoAnimate();
|
||||
|
||||
const getElementHeadline = (
|
||||
element: TSurveyElement,
|
||||
languageCode: string
|
||||
): (string | React.ReactElement)[] | string | undefined => {
|
||||
const headlineData = recallToHeadline(element.headline, localSurvey, true, languageCode);
|
||||
const headlineText = headlineData[languageCode];
|
||||
if (headlineText) {
|
||||
return formatTextWithSlashes(getTextContent(headlineText ?? ""));
|
||||
}
|
||||
return getTSurveyQuestionTypeEnumName(element.type, t);
|
||||
};
|
||||
|
||||
const shouldShowCautionAlert = (elementType: TSurveyElementTypeEnum): boolean => {
|
||||
return (
|
||||
responseCount > 0 &&
|
||||
[
|
||||
TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
TSurveyElementTypeEnum.MultipleChoiceMulti,
|
||||
TSurveyElementTypeEnum.PictureSelection,
|
||||
TSurveyElementTypeEnum.Rating,
|
||||
TSurveyElementTypeEnum.NPS,
|
||||
TSurveyElementTypeEnum.Ranking,
|
||||
TSurveyElementTypeEnum.Matrix,
|
||||
].includes(elementType)
|
||||
);
|
||||
};
|
||||
|
||||
const renderElementForm = (element: TSurveyElement, questionIdx: number) => {
|
||||
switch (element.type) {
|
||||
case TSurveyElementTypeEnum.OpenText:
|
||||
return (
|
||||
<OpenQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.MultipleChoiceSingle:
|
||||
return (
|
||||
<MultipleChoiceQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.MultipleChoiceMulti:
|
||||
return (
|
||||
<MultipleChoiceQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.NPS:
|
||||
return (
|
||||
<NPSQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.CTA:
|
||||
return (
|
||||
<CTAQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.Rating:
|
||||
return (
|
||||
<RatingQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.Consent:
|
||||
return (
|
||||
<ConsentQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.Date:
|
||||
return (
|
||||
<DateQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.PictureSelection:
|
||||
return (
|
||||
<PictureSelectionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.FileUpload:
|
||||
return (
|
||||
<FileUploadQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
project={project}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.Cal:
|
||||
return (
|
||||
<CalQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.Matrix:
|
||||
return (
|
||||
<MatrixQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.Address:
|
||||
return (
|
||||
<AddressQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.Ranking:
|
||||
return (
|
||||
<RankingQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.ContactInfo:
|
||||
return (
|
||||
<ContactInfoQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const style = {
|
||||
transition: transition ?? "transform 100ms ease",
|
||||
transform: CSS.Translate.toString(transform),
|
||||
@@ -155,7 +438,7 @@ export const BlockCard = ({
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
className={cn(
|
||||
hasInvalidElement ? "bg-red-400" : isBlockOpen ? "bg-slate-700" : "bg-slate-400",
|
||||
isBlockInvalid ? "bg-red-400" : isBlockOpen ? "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",
|
||||
"flex flex-col items-center justify-between gap-2"
|
||||
)}>
|
||||
@@ -188,9 +471,9 @@ export const BlockCard = ({
|
||||
</div>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<BlockMenu
|
||||
blockIndex={blockIdx}
|
||||
isFirstBlock={blockIdx === 0}
|
||||
isLastBlock={blockIdx === totalBlocks - 1}
|
||||
isOnlyBlock={totalBlocks === 1}
|
||||
onDuplicate={() => duplicateBlock(block.id)}
|
||||
onDelete={() => deleteBlock(block.id)}
|
||||
onMoveUp={() => moveBlock(block.id, "up")}
|
||||
@@ -211,13 +494,12 @@ export const BlockCard = ({
|
||||
}
|
||||
questionIdx += elementIndex;
|
||||
|
||||
const isInvalid = invalidQuestions ? invalidQuestions.includes(element.id) : false;
|
||||
const open = activeQuestionId === element.id;
|
||||
const isOpen = activeQuestionId === element.id;
|
||||
|
||||
return (
|
||||
<div key={element.id} className={cn(elementIndex > 0 && "border-t border-slate-200")}>
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
open={isOpen}
|
||||
onOpenChange={() => {
|
||||
if (activeQuestionId !== element.id) {
|
||||
setActiveQuestionId(element.id);
|
||||
@@ -229,7 +511,7 @@ export const BlockCard = ({
|
||||
<Collapsible.CollapsibleTrigger
|
||||
asChild
|
||||
className={cn(
|
||||
open ? "bg-slate-50" : "",
|
||||
isOpen ? "bg-slate-50" : "",
|
||||
"flex w-full cursor-pointer justify-between gap-4 p-4 hover:bg-slate-50"
|
||||
)}
|
||||
aria-label="Toggle question details">
|
||||
@@ -246,25 +528,9 @@ export const BlockCard = ({
|
||||
</p>
|
||||
)}
|
||||
<h3 className="text-sm font-semibold">
|
||||
{recallToHeadline(
|
||||
element.headline,
|
||||
localSurvey,
|
||||
true,
|
||||
selectedLanguageCode
|
||||
)[selectedLanguageCode]
|
||||
? formatTextWithSlashes(
|
||||
getTextContent(
|
||||
recallToHeadline(
|
||||
element.headline,
|
||||
localSurvey,
|
||||
true,
|
||||
selectedLanguageCode
|
||||
)[selectedLanguageCode] ?? ""
|
||||
)
|
||||
)
|
||||
: getTSurveyQuestionTypeEnumName(element.type, t)}
|
||||
{getElementHeadline(element, selectedLanguageCode)}
|
||||
</h3>
|
||||
{!open && (
|
||||
{!isOpen && (
|
||||
<p className="mt-1 truncate text-xs text-slate-500">
|
||||
{element?.required
|
||||
? t("environments.surveys.edit.required")
|
||||
@@ -302,226 +568,16 @@ export const BlockCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "pb-4"}`}>
|
||||
{responseCount > 0 &&
|
||||
[
|
||||
TSurveyElementTypeEnum.MultipleChoiceSingle,
|
||||
TSurveyElementTypeEnum.MultipleChoiceMulti,
|
||||
TSurveyElementTypeEnum.PictureSelection,
|
||||
TSurveyElementTypeEnum.Rating,
|
||||
TSurveyElementTypeEnum.NPS,
|
||||
TSurveyElementTypeEnum.Ranking,
|
||||
TSurveyElementTypeEnum.Matrix,
|
||||
].includes(element.type) ? (
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${isOpen && "pb-4"}`}>
|
||||
{shouldShowCautionAlert(element.type) && (
|
||||
<Alert variant="warning" size="small" className="w-fill" role="alert">
|
||||
<AlertTitle>{t("environments.surveys.edit.caution_text")}</AlertTitle>
|
||||
<AlertButton onClick={() => onAlertTrigger()}>
|
||||
{t("common.learn_more")}
|
||||
</AlertButton>
|
||||
</Alert>
|
||||
) : null}
|
||||
{element.type === TSurveyElementTypeEnum.OpenText ? (
|
||||
<OpenQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.MultipleChoiceSingle ? (
|
||||
<MultipleChoiceQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.MultipleChoiceMulti ? (
|
||||
<MultipleChoiceQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.NPS ? (
|
||||
<NPSQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.CTA ? (
|
||||
<CTAQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.Rating ? (
|
||||
<RatingQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.Consent ? (
|
||||
<ConsentQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.Date ? (
|
||||
<DateQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.PictureSelection ? (
|
||||
<PictureSelectionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.FileUpload ? (
|
||||
<FileUploadQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
project={project}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.Cal ? (
|
||||
<CalQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.Matrix ? (
|
||||
<MatrixQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.Address ? (
|
||||
<AddressQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.Ranking ? (
|
||||
<RankingQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.ContactInfo ? (
|
||||
<ContactInfoQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={element}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
) : null}
|
||||
)}
|
||||
{renderElementForm(element, questionIdx)}
|
||||
<div className="mt-4">
|
||||
<Collapsible.Root
|
||||
open={openAdvanced}
|
||||
|
||||
@@ -6,9 +6,9 @@ import { Button } from "@/modules/ui/components/button";
|
||||
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||
|
||||
interface BlockMenuProps {
|
||||
blockIndex: number;
|
||||
isFirstBlock: boolean;
|
||||
isLastBlock: boolean;
|
||||
isOnlyBlock: boolean;
|
||||
onDuplicate: () => void;
|
||||
onDelete: () => void;
|
||||
onMoveUp: () => void;
|
||||
@@ -16,9 +16,9 @@ interface BlockMenuProps {
|
||||
}
|
||||
|
||||
export const BlockMenu = ({
|
||||
blockIndex,
|
||||
isFirstBlock,
|
||||
isLastBlock,
|
||||
isOnlyBlock,
|
||||
onDuplicate,
|
||||
onDelete,
|
||||
onMoveUp,
|
||||
@@ -77,9 +77,12 @@ export const BlockMenu = ({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={isOnlyBlock}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
if (!isOnlyBlock) {
|
||||
e.stopPropagation();
|
||||
onDelete();
|
||||
}
|
||||
}}
|
||||
className="h-8 w-8">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
|
||||
@@ -8,6 +8,7 @@ import { TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyBlock, TSurveyBlockLogic } from "@formbricks/types/surveys/blocks";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { addMultiLanguageLabels, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
|
||||
import { ConditionalLogic } from "@/modules/survey/editor/components/conditional-logic";
|
||||
|
||||
@@ -102,11 +103,15 @@ export const BlockSettings = ({
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
onBlur={(e) => {
|
||||
if (!block.backButtonLabel) return;
|
||||
const translatedBackButtonLabel = {
|
||||
...block.backButtonLabel,
|
||||
[selectedLanguageCode]: e.target.value,
|
||||
};
|
||||
const languageSymbols = extractLanguageCodes(localSurvey.languages ?? []);
|
||||
const existingLabel = block.backButtonLabel || {};
|
||||
const translatedBackButtonLabel = addMultiLanguageLabels(
|
||||
{
|
||||
...existingLabel,
|
||||
[selectedLanguageCode]: e.target.value,
|
||||
},
|
||||
languageSymbols
|
||||
);
|
||||
updateBlockButtonLabel(blockIndex, "backButtonLabel", translatedBackButtonLabel);
|
||||
updateEmptyButtonLabels("backButtonLabel", translatedBackButtonLabel, blockIndex);
|
||||
}}
|
||||
@@ -121,11 +126,17 @@ export const BlockSettings = ({
|
||||
isInvalid={false}
|
||||
updateQuestion={(_, updatedAttributes) => {
|
||||
if ("buttonLabel" in updatedAttributes) {
|
||||
const languageSymbols = extractLanguageCodes(localSurvey.languages ?? []);
|
||||
const buttonLabel = updatedAttributes.buttonLabel as TI18nString;
|
||||
updateBlockButtonLabel(blockIndex, "buttonLabel", {
|
||||
...block.buttonLabel,
|
||||
[selectedLanguageCode]: buttonLabel[selectedLanguageCode],
|
||||
});
|
||||
const existingLabel = block.buttonLabel || {};
|
||||
const updatedButtonLabel = addMultiLanguageLabels(
|
||||
{
|
||||
...existingLabel,
|
||||
[selectedLanguageCode]: buttonLabel[selectedLanguageCode],
|
||||
},
|
||||
languageSymbols
|
||||
);
|
||||
updateBlockButtonLabel(blockIndex, "buttonLabel", updatedButtonLabel);
|
||||
}
|
||||
}}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
@@ -134,11 +145,15 @@ export const BlockSettings = ({
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
onBlur={(e) => {
|
||||
if (!block.buttonLabel) return;
|
||||
const translatedNextButtonLabel = {
|
||||
...block.buttonLabel,
|
||||
[selectedLanguageCode]: e.target.value,
|
||||
};
|
||||
const languageSymbols = extractLanguageCodes(localSurvey.languages ?? []);
|
||||
const existingLabel = block.buttonLabel || {};
|
||||
const translatedNextButtonLabel = addMultiLanguageLabels(
|
||||
{
|
||||
...existingLabel,
|
||||
[selectedLanguageCode]: e.target.value,
|
||||
},
|
||||
languageSymbols
|
||||
);
|
||||
updateBlockButtonLabel(blockIndex, "buttonLabel", translatedNextButtonLabel);
|
||||
// Don't propagate to last block
|
||||
const lastBlockIndex = localSurvey.blocks.length - 1;
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { HashIcon, PlusIcon, SmileIcon, StarIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyRatingElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
@@ -26,7 +25,6 @@ interface RatingQuestionFormProps {
|
||||
locale: TUserLocale;
|
||||
isStorageConfigured: boolean;
|
||||
isExternalUrlsAllowed?: boolean;
|
||||
buttonLabel?: TI18nString;
|
||||
}
|
||||
|
||||
export const RatingQuestionForm = ({
|
||||
@@ -40,7 +38,6 @@ export const RatingQuestionForm = ({
|
||||
locale,
|
||||
isStorageConfigured = true,
|
||||
isExternalUrlsAllowed,
|
||||
buttonLabel,
|
||||
}: RatingQuestionFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
@@ -181,27 +178,6 @@ export const RatingQuestionForm = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
{!question.required && (
|
||||
<div className="flex-1">
|
||||
<QuestionFormInput
|
||||
id="buttonLabel"
|
||||
value={buttonLabel}
|
||||
label={t("environments.surveys.edit.next_button_label")}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
placeholder={"skip"}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{question.scale !== "star" && (
|
||||
<AdvancedOptionToggle
|
||||
isChecked={question.isColorCodingEnabled}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -130,6 +130,11 @@ export const updateBlock = (
|
||||
* @returns Result with updated survey or Error
|
||||
*/
|
||||
export const deleteBlock = (survey: TSurvey, blockId: string): Result<TSurvey, Error> => {
|
||||
// Prevent deleting the last block
|
||||
if (survey.blocks?.length === 1) {
|
||||
return err(new Error("Cannot delete the last block in the survey"));
|
||||
}
|
||||
|
||||
const filteredBlocks = survey.blocks?.filter((b) => b.id !== blockId) || [];
|
||||
|
||||
if (filteredBlocks.length === survey.blocks?.length) {
|
||||
|
||||
@@ -27,7 +27,6 @@ interface BlockConditionalProps {
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
surveyId: string;
|
||||
autoFocusEnabled: boolean;
|
||||
currentBlockId: string;
|
||||
isBackButtonHidden: boolean;
|
||||
onOpenExternalURL?: (url: string) => void | Promise<void>;
|
||||
dir?: "ltr" | "rtl" | "auto";
|
||||
@@ -208,8 +207,6 @@ export function BlockConditional({
|
||||
onChange={(responseData) => handleElementChange(element.id, responseData)}
|
||||
onBack={() => {}}
|
||||
onFileUpload={onFileUpload}
|
||||
isFirstElement={false}
|
||||
isLastElement={false}
|
||||
languageCode={languageCode}
|
||||
prefilledElementValue={prefilledResponseData?.[element.id]}
|
||||
skipPrefilled={skipPrefilled}
|
||||
|
||||
@@ -30,8 +30,6 @@ interface ElementConditionalProps {
|
||||
onChange: (responseData: TResponseData) => void;
|
||||
onBack: () => void;
|
||||
onFileUpload: (file: TJsFileUploadParams["file"], config?: TUploadFileConfig) => Promise<string>;
|
||||
isFirstElement: boolean;
|
||||
isLastElement: boolean;
|
||||
languageCode: string;
|
||||
prefilledElementValue?: TResponseDataValue;
|
||||
skipPrefilled?: boolean;
|
||||
@@ -113,195 +111,228 @@ export function ElementConditional({
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
{element.type === TSurveyElementTypeEnum.OpenText ? (
|
||||
<OpenTextQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
dir={dir}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.MultipleChoiceSingle ? (
|
||||
<MultipleChoiceSingleQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={typeof value === "string" ? value : undefined}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
dir={dir}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.MultipleChoiceMulti ? (
|
||||
<MultipleChoiceMultiQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={Array.isArray(value) ? value : []}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
dir={dir}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.NPS ? (
|
||||
<NPSQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={typeof value === "number" ? value : undefined}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
dir={dir}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.CTA ? (
|
||||
<CTAQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
onOpenExternalURL={onOpenExternalURL}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.Rating ? (
|
||||
<RatingQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={typeof value === "number" ? value : undefined}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
dir={dir}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.Consent ? (
|
||||
<ConsentQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
dir={dir}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.Date ? (
|
||||
<DateQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.PictureSelection ? (
|
||||
<PictureSelectionQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={Array.isArray(value) ? value : []}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
dir={dir}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.FileUpload ? (
|
||||
<FileUploadQuestion
|
||||
key={element.id}
|
||||
surveyId={surveyId}
|
||||
question={element}
|
||||
value={Array.isArray(value) ? value : []}
|
||||
onChange={onChange}
|
||||
onFileUpload={onFileUpload}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.Cal ? (
|
||||
<CalQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
currentQuestionId={currentElementId}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.Matrix ? (
|
||||
<MatrixQuestion
|
||||
question={element}
|
||||
value={typeof value === "object" && !Array.isArray(value) ? value : {}}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
currentQuestionId={currentElementId}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.Address ? (
|
||||
<AddressQuestion
|
||||
question={element}
|
||||
value={Array.isArray(value) ? value : undefined}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
currentQuestionId={currentElementId}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
dir={dir}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.Ranking ? (
|
||||
<RankingQuestion
|
||||
question={element}
|
||||
value={Array.isArray(value) ? getResponseValueForRankingQuestion(value, element.choices) : []}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
/>
|
||||
) : element.type === TSurveyElementTypeEnum.ContactInfo ? (
|
||||
<ContactInfoQuestion
|
||||
question={element}
|
||||
value={Array.isArray(value) ? value : undefined}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
currentQuestionId={currentElementId}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
dir={dir}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
const renderElement = () => {
|
||||
switch (element.type) {
|
||||
case TSurveyElementTypeEnum.OpenText:
|
||||
return (
|
||||
<OpenTextQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
dir={dir}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.MultipleChoiceSingle:
|
||||
return (
|
||||
<MultipleChoiceSingleQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={typeof value === "string" ? value : undefined}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
dir={dir}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.MultipleChoiceMulti:
|
||||
return (
|
||||
<MultipleChoiceMultiQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={Array.isArray(value) ? value : []}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
dir={dir}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.NPS:
|
||||
return (
|
||||
<NPSQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={typeof value === "number" ? value : undefined}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
dir={dir}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.CTA:
|
||||
return (
|
||||
<CTAQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
onOpenExternalURL={onOpenExternalURL}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.Rating:
|
||||
return (
|
||||
<RatingQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={typeof value === "number" ? value : undefined}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
dir={dir}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.Consent:
|
||||
return (
|
||||
<ConsentQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
dir={dir}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.Date:
|
||||
return (
|
||||
<DateQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.PictureSelection:
|
||||
return (
|
||||
<PictureSelectionQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={Array.isArray(value) ? value : []}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
dir={dir}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.FileUpload:
|
||||
return (
|
||||
<FileUploadQuestion
|
||||
key={element.id}
|
||||
surveyId={surveyId}
|
||||
question={element}
|
||||
value={Array.isArray(value) ? value : []}
|
||||
onChange={onChange}
|
||||
onFileUpload={onFileUpload}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.Cal:
|
||||
return (
|
||||
<CalQuestion
|
||||
key={element.id}
|
||||
question={element}
|
||||
value={typeof value === "string" ? value : ""}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
currentQuestionId={currentElementId}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.Matrix:
|
||||
return (
|
||||
<MatrixQuestion
|
||||
question={element}
|
||||
value={typeof value === "object" && !Array.isArray(value) ? value : {}}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
currentQuestionId={currentElementId}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.Address:
|
||||
return (
|
||||
<AddressQuestion
|
||||
question={element}
|
||||
value={Array.isArray(value) ? value : undefined}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
currentQuestionId={currentElementId}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
dir={dir}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.Ranking:
|
||||
return (
|
||||
<RankingQuestion
|
||||
question={element}
|
||||
value={Array.isArray(value) ? getResponseValueForRankingQuestion(value, element.choices) : []}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentQuestionId={currentElementId}
|
||||
/>
|
||||
);
|
||||
case TSurveyElementTypeEnum.ContactInfo:
|
||||
return (
|
||||
<ContactInfoQuestion
|
||||
question={element}
|
||||
value={Array.isArray(value) ? value : undefined}
|
||||
onChange={onChange}
|
||||
languageCode={languageCode}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
currentQuestionId={currentElementId}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
dir={dir}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return <div ref={containerRef}>{renderElement()}</div>;
|
||||
}
|
||||
|
||||
@@ -777,7 +777,6 @@ export function Survey({
|
||||
isLastBlock={block.id === localSurvey.blocks[localSurvey.blocks.length - 1].id}
|
||||
languageCode={selectedLanguage}
|
||||
autoFocusEnabled={autoFocusEnabled}
|
||||
currentBlockId={blockId}
|
||||
isBackButtonHidden={localSurvey.isBackButtonHidden}
|
||||
onOpenExternalURL={onOpenExternalURL}
|
||||
dir={dir}
|
||||
|
||||
@@ -11,7 +11,7 @@ export const LinkIcon = ({ className }: LinkIconProps) => {
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}>
|
||||
|
||||
@@ -36,44 +36,58 @@ export function AddressQuestion({
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = question.imageUrl || question.videoUrl;
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
const safeValue = useMemo(() => {
|
||||
return Array.isArray(value) ? value : ["", "", "", "", "", ""];
|
||||
}, [value]);
|
||||
|
||||
const isCurrent = question.id === currentQuestionId;
|
||||
|
||||
const fields = [
|
||||
{
|
||||
id: "addressLine1",
|
||||
...question.addressLine1,
|
||||
label: question.addressLine1.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "addressLine2",
|
||||
...question.addressLine2,
|
||||
label: question.addressLine2.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "city",
|
||||
...question.city,
|
||||
label: question.city.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "state",
|
||||
...question.state,
|
||||
label: question.state.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "zip",
|
||||
...question.zip,
|
||||
label: question.zip.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "country",
|
||||
...question.country,
|
||||
label: question.country.placeholder[languageCode],
|
||||
},
|
||||
];
|
||||
const fields = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: "addressLine1",
|
||||
...question.addressLine1,
|
||||
label: question.addressLine1.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "addressLine2",
|
||||
...question.addressLine2,
|
||||
label: question.addressLine2.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "city",
|
||||
...question.city,
|
||||
label: question.city.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "state",
|
||||
...question.state,
|
||||
label: question.state.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "zip",
|
||||
...question.zip,
|
||||
label: question.zip.placeholder[languageCode],
|
||||
},
|
||||
{
|
||||
id: "country",
|
||||
...question.country,
|
||||
label: question.country.placeholder[languageCode],
|
||||
},
|
||||
],
|
||||
[
|
||||
question.addressLine1,
|
||||
question.addressLine2,
|
||||
question.city,
|
||||
question.state,
|
||||
question.zip,
|
||||
question.country,
|
||||
languageCode,
|
||||
]
|
||||
);
|
||||
|
||||
const handleChange = (fieldId: string, fieldValue: string) => {
|
||||
const newValue = fields.map((field) => {
|
||||
@@ -102,6 +116,25 @@ export function AddressQuestion({
|
||||
[question.id, autoFocusEnabled, currentQuestionId]
|
||||
);
|
||||
|
||||
const isFieldRequired = useCallback(
|
||||
(field: (typeof fields)[number]) => {
|
||||
if (field.required) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if all fields are optional and the question is required, then the fields should be required
|
||||
if (
|
||||
fields.filter((currField) => currField.show).every((currField) => !currField.required) &&
|
||||
question.required
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
[fields, question.required]
|
||||
);
|
||||
|
||||
return (
|
||||
<form key={question.id} onSubmit={handleSubmit} className="fb-w-full" ref={formRef}>
|
||||
<div>
|
||||
@@ -118,32 +151,17 @@ export function AddressQuestion({
|
||||
|
||||
<div className="fb-flex fb-flex-col fb-space-y-2 fb-mt-4 fb-w-full">
|
||||
{fields.map((field, index) => {
|
||||
const isFieldRequired = () => {
|
||||
if (field.required) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if all fields are optional and the question is required, then the fields should be required
|
||||
if (
|
||||
fields.filter((currField) => currField.show).every((currField) => !currField.required) &&
|
||||
question.required
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
const isRequired = isFieldRequired(field);
|
||||
|
||||
return (
|
||||
field.show && (
|
||||
<div className="fb-space-y-1">
|
||||
<Label htmlForId={field.id} text={isFieldRequired() ? `${field.label}*` : field.label} />
|
||||
<div className="fb-space-y-1" key={field.id}>
|
||||
<Label htmlForId={field.id} text={isRequired ? `${field.label}*` : field.label} />
|
||||
<Input
|
||||
id={field.id}
|
||||
key={field.id}
|
||||
required={isFieldRequired()}
|
||||
required={isRequired}
|
||||
value={safeValue[index] || ""}
|
||||
type={field.id === "email" ? "email" : "text"}
|
||||
type="text"
|
||||
onChange={(e) => {
|
||||
handleChange(field.id, e.currentTarget.value);
|
||||
}}
|
||||
|
||||
@@ -35,10 +35,11 @@ export function CalQuestion({
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
|
||||
|
||||
const onSuccessfulBooking = useCallback(() => {
|
||||
setErrorMessage("");
|
||||
onChange({ [question.id]: "booked" });
|
||||
const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedttc);
|
||||
}, [onChange, question.id, setTtc, startTime, ttc]);
|
||||
}, [onChange, question.id, setTtc, startTime, ttc, setErrorMessage]);
|
||||
|
||||
return (
|
||||
<form
|
||||
|
||||
@@ -96,7 +96,7 @@ export function ConsentQuestion({
|
||||
aria-labelledby={`${question.id}-label`}
|
||||
required={question.required}
|
||||
/>
|
||||
<span id={`${question.id}-label`} className="fb-ml-3 fb-mr-3 fb-font-medium fb-flex-1" dir="auto">
|
||||
<span className="fb-ml-3 fb-mr-3 fb-font-medium fb-flex-1" dir="auto">
|
||||
{getLocalizedValue(question.label, languageCode)}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
@@ -127,10 +127,14 @@ export function DateQuestion({
|
||||
key={question.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Validate required field
|
||||
if (question.required && !value) {
|
||||
setErrorMessage(t("errors.please_select_a_date"));
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorMessage("");
|
||||
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedTtcObj);
|
||||
}}
|
||||
@@ -189,6 +193,15 @@ export function DateQuestion({
|
||||
isOpen={datePickerOpen}
|
||||
onChange={(value) => {
|
||||
const date = value as Date;
|
||||
|
||||
if (!date) {
|
||||
if (question.required) {
|
||||
setErrorMessage(t("errors.please_select_a_date"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorMessage("");
|
||||
setSelectedDate(date);
|
||||
|
||||
// Get the timezone offset in minutes and convert it to milliseconds
|
||||
|
||||
@@ -237,9 +237,8 @@ export function MultipleChoiceMultiQuestion({
|
||||
baseLabelClassName
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
// Accessibility: if spacebar was pressed pass this down to the input
|
||||
// Accessibility: if spacebar was pressed pass this down to the checkbox
|
||||
if (e.key === " ") {
|
||||
if (otherSelected) return;
|
||||
e.preventDefault();
|
||||
document.getElementById(otherOption.id)?.click();
|
||||
}
|
||||
@@ -248,7 +247,7 @@ export function MultipleChoiceMultiQuestion({
|
||||
<input
|
||||
type="checkbox"
|
||||
dir={dir}
|
||||
tabIndex={-1}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
id={otherOption.id}
|
||||
name={question.id}
|
||||
value={getLocalizedValue(otherOption.label, languageCode)}
|
||||
@@ -279,7 +278,7 @@ export function MultipleChoiceMultiQuestion({
|
||||
<input
|
||||
ref={otherSpecify}
|
||||
dir={otherOptionInputDir}
|
||||
id={`${otherOption.id}-label`}
|
||||
id={`${otherOption.id}-specify`}
|
||||
maxLength={250}
|
||||
name={question.id}
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
|
||||
@@ -180,9 +180,7 @@ export function MultipleChoiceSingleQuestion({
|
||||
<label
|
||||
tabIndex={isCurrent ? 0 : -1}
|
||||
className={cn(
|
||||
value === getLocalizedValue(otherOption.label, languageCode)
|
||||
? "fb-border-brand fb-bg-input-bg-selected fb-z-10"
|
||||
: "fb-border-border",
|
||||
otherSelected ? "fb-border-brand fb-bg-input-bg-selected fb-z-10" : "fb-border-border",
|
||||
"fb-text-heading focus-within:fb-border-brand fb-bg-input-bg focus-within:fb-bg-input-bg-selected hover:fb-bg-input-bg-selected fb-rounded-custom fb-relative fb-flex fb-cursor-pointer fb-flex-col fb-border fb-p-4 focus:fb-outline-none"
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
@@ -224,11 +222,11 @@ export function MultipleChoiceSingleQuestion({
|
||||
{otherSelected ? (
|
||||
<input
|
||||
ref={otherSpecify}
|
||||
id={`${otherOption.id}-label`}
|
||||
id={`${otherOption.id}-input`}
|
||||
dir={otherOptionInputDir}
|
||||
name={question.id}
|
||||
pattern=".*\S+.*"
|
||||
value={value}
|
||||
value={value ?? ""}
|
||||
onChange={(e) => {
|
||||
onChange({ [question.id]: e.currentTarget.value });
|
||||
}}
|
||||
|
||||
@@ -169,7 +169,7 @@ export function OpenTextQuestion({
|
||||
)}
|
||||
{question.inputType === "text" && question.charLimit?.max !== undefined && (
|
||||
<span
|
||||
className={`fb-text-xs ${currentLength >= question.charLimit?.max ? "fb-text-red-500 font-semibold" : "text-neutral-400"}`}>
|
||||
className={`fb-text-xs ${currentLength >= question.charLimit?.max ? "fb-text-red-500 fb-font-semibold" : "fb-text-neutral-400"}`}>
|
||||
{currentLength}/{question.charLimit?.max}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -45,9 +45,11 @@ export function StackedCardsContainer({
|
||||
|
||||
const blockIdxTemp = useMemo(() => {
|
||||
if (currentBlockId === "start") return survey.welcomeCard.enabled ? -1 : 0;
|
||||
|
||||
if (!survey.blocks.map((block) => block.id).includes(currentBlockId)) {
|
||||
return survey.blocks.length;
|
||||
}
|
||||
|
||||
return survey.blocks.findIndex((block) => block.id === currentBlockId);
|
||||
}, [currentBlockId, survey]);
|
||||
|
||||
@@ -132,7 +134,7 @@ export function StackedCardsContainer({
|
||||
// Reset block progress, when card arrangement changes
|
||||
useEffect(() => {
|
||||
if (shouldResetBlockId) {
|
||||
setBlockId(survey.welcomeCard.enabled ? "start" : survey.blocks[0]?.id);
|
||||
setBlockId(survey.welcomeCard.enabled ? "start" : survey.blocks[0].id);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Only update when cardArrangement changes
|
||||
}, [cardArrangement]);
|
||||
|
||||
@@ -1199,7 +1199,10 @@ export const ZSurvey = z
|
||||
// Validate block button labels
|
||||
const defaultLanguageCode = "default";
|
||||
|
||||
if (block.buttonLabel && block.buttonLabel[defaultLanguageCode].trim() !== "") {
|
||||
if (
|
||||
block.buttonLabel?.[defaultLanguageCode] &&
|
||||
block.buttonLabel[defaultLanguageCode].trim() !== ""
|
||||
) {
|
||||
// Validate button label for all enabled languages
|
||||
const enabledLanguages = languages.filter((lang) => lang.enabled);
|
||||
const languageCodes = enabledLanguages.map((lang) =>
|
||||
@@ -1224,7 +1227,10 @@ export const ZSurvey = z
|
||||
}
|
||||
}
|
||||
|
||||
if (block.backButtonLabel && block.backButtonLabel[defaultLanguageCode].trim() !== "") {
|
||||
if (
|
||||
block.backButtonLabel?.[defaultLanguageCode] &&
|
||||
block.backButtonLabel[defaultLanguageCode].trim() !== ""
|
||||
) {
|
||||
// Validate back button label for all enabled languages
|
||||
const enabledLanguages = languages.filter((lang) => lang.enabled);
|
||||
const languageCodes = enabledLanguages.map((lang) =>
|
||||
|
||||
Reference in New Issue
Block a user