code cleanup

This commit is contained in:
pandeymangg
2025-11-25 22:31:44 +05:30
parent 7770d43f9a
commit 9f59d7a967
131 changed files with 3103 additions and 3072 deletions

View File

@@ -2,14 +2,14 @@
import React, { createContext, useCallback, useContext, useState } from "react";
import {
QuestionOption,
QuestionOptions,
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
import { QuestionFilterOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter";
ElementOption,
ElementOptions,
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ElementsComboBox";
import { ElementFilterOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter";
import { getTodayDate } from "@/app/lib/surveys/surveys";
export interface FilterValue {
questionType: Partial<QuestionOption>;
elementType: Partial<ElementOption>;
filterType: {
filterValue: string | undefined;
filterComboBoxValue: string | string[] | undefined;
@@ -24,8 +24,8 @@ export interface SelectedFilterValue {
}
interface SelectedFilterOptions {
questionOptions: QuestionOptions[];
questionFilterOptions: QuestionFilterOptions[];
elementOptions: ElementOptions[];
elementFilterOptions: ElementFilterOptions[];
}
export interface DateRange {
@@ -53,8 +53,8 @@ const ResponseFilterProvider = ({ children }: { children: React.ReactNode }) =>
});
// state holds all the options of the responses fetched
const [selectedOptions, setSelectedOptions] = useState<SelectedFilterOptions>({
questionFilterOptions: [],
questionOptions: [],
elementFilterOptions: [],
elementOptions: [],
});
const [dateRange, setDateRange] = useState<DateRange>({

View File

@@ -24,7 +24,7 @@ import NotionLogo from "@/images/notion.png";
import { structuredClone } from "@/lib/pollyfills/structuredClone";
import { recallToHeadline } from "@/lib/utils/recall";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { getQuestionTypes } from "@/modules/survey/lib/questions";
import { getElementTypes } from "@/modules/survey/lib/elements";
import { Button } from "@/modules/ui/components/button";
import {
Dialog,
@@ -307,7 +307,7 @@ export const AddIntegrationModal = ({
</>
);
case ERRORS.MAPPING:
const question = getQuestionTypes(t).find((qt) => qt.id === ques.type);
const question = getElementTypes(t).find((qt) => qt.id === ques.type);
if (!question) return null;
return (
<>

View File

@@ -56,12 +56,11 @@ export const formatContactInfoData = (responseValue: TResponseDataValue): Record
export const extractResponseData = (response: TResponseWithQuotas, survey: TSurvey): Record<string, any> => {
const responseData: Record<string, any> = {};
// Derive questions from blocks
const questions = getElementsFromBlocks(survey.blocks);
const elements = getElementsFromBlocks(survey.blocks);
for (const question of questions) {
const responseValue = response.data[question.id];
switch (question.type) {
for (const element of elements) {
const responseValue = response.data[element.id];
switch (element.type) {
case "matrix":
if (typeof responseValue === "object") {
Object.assign(responseData, responseValue);
@@ -74,7 +73,7 @@ export const extractResponseData = (response: TResponseWithQuotas, survey: TSurv
Object.assign(responseData, formatContactInfoData(responseValue));
break;
default:
responseData[question.id] = responseValue;
responseData[element.id] = responseValue;
}
}

View File

@@ -15,7 +15,7 @@ import { getFormattedDateTimeString } from "@/lib/utils/datetime";
import { recallToHeadline } from "@/lib/utils/recall";
import { RenderResponse } from "@/modules/analysis/components/SingleResponseCard/components/RenderResponse";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { VARIABLES_ICON_MAP, getQuestionIconMap } from "@/modules/survey/lib/questions";
import { VARIABLES_ICON_MAP, getElementIconMap } from "@/modules/survey/lib/elements";
import { getSelectionColumn } from "@/modules/ui/components/data-table";
import { IdBadge } from "@/modules/ui/components/id-badge";
import { ResponseBadges } from "@/modules/ui/components/response-badges";
@@ -30,35 +30,33 @@ import {
getMetadataValue,
} from "../lib/utils";
const getQuestionColumnsData = (
question: TSurveyElement,
const getElementColumnsData = (
element: TSurveyElement,
survey: TSurvey,
isExpanded: boolean,
t: TFunction
): ColumnDef<TResponseTableData>[] => {
const QUESTIONS_ICON_MAP = getQuestionIconMap(t);
const ELEMENTS_ICON_MAP = getElementIconMap(t);
const addressFields = ["addressLine1", "addressLine2", "city", "state", "zip", "country"];
const contactInfoFields = ["firstName", "lastName", "email", "phone", "company"];
// Helper function to create consistent column headers
const createQuestionHeader = (questionType: string, headline: string, suffix?: string) => {
const createElementHeader = (elementType: string, headline: string, suffix?: string) => {
const title = suffix ? `${headline} - ${suffix}` : headline;
const QuestionHeader = () => (
const ElementHeader = () => (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2 overflow-hidden">
<span className="h-4 w-4">{QUESTIONS_ICON_MAP[questionType]}</span>
<span className="h-4 w-4">{ELEMENTS_ICON_MAP[elementType]}</span>
<span className="truncate">{title}</span>
</div>
</div>
);
QuestionHeader.displayName = "QuestionHeader";
return QuestionHeader;
return ElementHeader;
};
// Helper function to get localized question headline
const getQuestionHeadline = (question: TSurveyElement, survey: TSurvey) => {
const getElementHeadline = (element: TSurveyElement, survey: TSurvey) => {
return getTextContent(
getLocalizedValue(recallToHeadline(question.headline, survey, false, "default"), "default")
getLocalizedValue(recallToHeadline(element.headline, survey, false, "default"), "default")
);
};
@@ -77,18 +75,18 @@ const getQuestionColumnsData = (
);
};
switch (question.type) {
switch (element.type) {
case "matrix":
return question.rows.map((matrixRow) => {
return element.rows.map((matrixRow) => {
return {
accessorKey: "QUESTION_" + question.id + "_" + matrixRow.label.default,
accessorKey: "ELEMENT_" + element.id + "_" + matrixRow.label.default,
header: () => {
return (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2 overflow-hidden">
<span className="h-4 w-4">{QUESTIONS_ICON_MAP["matrix"]}</span>
<span className="h-4 w-4">{ELEMENTS_ICON_MAP["matrix"]}</span>
<span className="truncate">
{getTextContent(getLocalizedValue(question.headline, "default")) +
{getTextContent(getLocalizedValue(element.headline, "default")) +
" - " +
getLocalizedValue(matrixRow.label, "default")}
</span>
@@ -108,12 +106,12 @@ const getQuestionColumnsData = (
case "address":
return addressFields.map((addressField) => {
return {
accessorKey: "QUESTION_" + question.id + "_" + addressField,
accessorKey: "ELEMENT_" + element.id + "_" + addressField,
header: () => {
return (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2 overflow-hidden">
<span className="h-4 w-4">{QUESTIONS_ICON_MAP["address"]}</span>
<span className="h-4 w-4">{ELEMENTS_ICON_MAP["address"]}</span>
<span className="truncate">{getAddressFieldLabel(addressField, t)}</span>
</div>
</div>
@@ -131,12 +129,12 @@ const getQuestionColumnsData = (
case "contactInfo":
return contactInfoFields.map((contactInfoField) => {
return {
accessorKey: "QUESTION_" + question.id + "_" + contactInfoField,
accessorKey: "ELEMENT_" + element.id + "_" + contactInfoField,
header: () => {
return (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2 overflow-hidden">
<span className="h-4 w-4">{QUESTIONS_ICON_MAP["contactInfo"]}</span>
<span className="h-4 w-4">{ELEMENTS_ICON_MAP["contactInfo"]}</span>
<span className="truncate">{getContactInfoFieldLabel(contactInfoField, t)}</span>
</div>
</div>
@@ -155,17 +153,17 @@ const getQuestionColumnsData = (
case "multipleChoiceSingle":
case "ranking":
case "pictureSelection": {
const questionHeadline = getQuestionHeadline(question, survey);
const elementHeadline = getElementHeadline(element, survey);
return [
{
accessorKey: "QUESTION_" + question.id,
header: createQuestionHeader(question.type, questionHeadline),
accessorKey: "ELEMENT_" + element.id,
header: createElementHeader(element.type, elementHeadline),
cell: ({ row }) => {
const responseValue = row.original.responseData[question.id];
const responseValue = row.original.responseData[element.id];
const language = row.original.language;
return (
<RenderResponse
question={question}
element={element}
survey={survey}
responseData={responseValue}
language={language}
@@ -176,15 +174,15 @@ const getQuestionColumnsData = (
},
},
{
accessorKey: "QUESTION_" + question.id + "optionIds",
header: createQuestionHeader(question.type, questionHeadline, t("common.option_id")),
accessorKey: "ELEMENT_" + element.id + "optionIds",
header: createElementHeader(element.type, elementHeadline, t("common.option_id")),
cell: ({ row }) => {
const responseValue = row.original.responseData[question.id];
const responseValue = row.original.responseData[element.id];
// Type guard to ensure responseValue is the correct type
if (typeof responseValue === "string" || Array.isArray(responseValue)) {
const choiceIds = extractChoiceIdsFromResponse(
responseValue,
question,
element,
row.original.language || undefined
);
return renderChoiceIdBadges(choiceIds, isExpanded);
@@ -198,28 +196,25 @@ const getQuestionColumnsData = (
default:
return [
{
accessorKey: "QUESTION_" + question.id,
accessorKey: "ELEMENT_" + element.id,
header: () => (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2 overflow-hidden">
<span className="h-4 w-4">{QUESTIONS_ICON_MAP[question.type]}</span>
<span className="h-4 w-4">{ELEMENTS_ICON_MAP[element.type]}</span>
<span className="truncate">
{getTextContent(
getLocalizedValue(
recallToHeadline(question.headline, survey, false, "default"),
"default"
)
getLocalizedValue(recallToHeadline(element.headline, survey, false, "default"), "default")
)}
</span>
</div>
</div>
),
cell: ({ row }) => {
const responseValue = row.original.responseData[question.id];
const responseValue = row.original.responseData[element.id];
const language = row.original.language;
return (
<RenderResponse
question={question}
element={element}
survey={survey}
responseData={responseValue}
language={language}
@@ -267,10 +262,8 @@ export const generateResponseTableColumns = (
t: TFunction,
showQuotasColumn: boolean
): ColumnDef<TResponseTableData>[] => {
const questions = getElementsFromBlocks(survey.blocks);
const questionColumns = questions.flatMap((question) =>
getQuestionColumnsData(question, survey, isExpanded, t)
);
const elements = getElementsFromBlocks(survey.blocks);
const elementColumns = elements.flatMap((element) => getElementColumnsData(element, survey, isExpanded, t));
const dateColumn: ColumnDef<TResponseTableData> = {
accessorKey: "createdAt",
@@ -417,7 +410,7 @@ export const generateResponseTableColumns = (
),
};
// Combine the selection column with the dynamic question columns
// Combine the selection column with the dynamic element columns
const baseColumns = [
personColumn,
singleUseIdColumn,
@@ -425,7 +418,7 @@ export const generateResponseTableColumns = (
...(showQuotasColumn ? [quotasColumn] : []),
statusColumn,
...(survey.isVerifyEmailEnabled ? [verifiedEmailColumn] : []),
...questionColumns,
...elementColumns,
...variableColumns,
...hiddenFieldColumns,
...metadataColumns,

View File

@@ -8,20 +8,20 @@ import { timeSince } from "@/lib/time";
import { getContactIdentifier } from "@/lib/utils/contact";
import { ArrayResponse } from "@/modules/ui/components/array-response";
import { PersonAvatar } from "@/modules/ui/components/avatars";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
import { ElementSummaryHeader } from "./ElementSummaryHeader";
interface AddressSummaryProps {
questionSummary: TSurveyElementSummaryAddress;
elementSummary: TSurveyElementSummaryAddress;
environmentId: string;
survey: TSurvey;
locale: TUserLocale;
}
export const AddressSummary = ({ questionSummary, environmentId, survey, locale }: AddressSummaryProps) => {
export const AddressSummary = ({ elementSummary, environmentId, survey, locale }: AddressSummaryProps) => {
const { t } = useTranslation();
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
<div>
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">
<div className="pl-4 md:pl-6">{t("common.user")}</div>
@@ -29,7 +29,7 @@ export const AddressSummary = ({ questionSummary, environmentId, survey, locale
<div className="px-4 md:px-6">{t("common.time")}</div>
</div>
<div className="max-h-[62vh] w-full overflow-y-auto">
{questionSummary.samples.map((response) => {
{elementSummary.samples.map((response) => {
return (
<div
key={response.id}

View File

@@ -5,36 +5,36 @@ import { useTranslation } from "react-i18next";
import { TSurvey, TSurveyElementSummaryCta } from "@formbricks/types/surveys/types";
import { ProgressBar } from "@/modules/ui/components/progress-bar";
import { convertFloatToNDecimal } from "../lib/utils";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
import { ElementSummaryHeader } from "./ElementSummaryHeader";
interface CTASummaryProps {
questionSummary: TSurveyElementSummaryCta;
elementSummary: TSurveyElementSummaryCta;
survey: TSurvey;
}
export const CTASummary = ({ questionSummary, survey }: CTASummaryProps) => {
export const CTASummary = ({ elementSummary, survey }: CTASummaryProps) => {
const { t } = useTranslation();
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader
<ElementSummaryHeader
survey={survey}
questionSummary={questionSummary}
elementSummary={elementSummary}
showResponses={false}
additionalInfo={
<>
<div className="flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{`${questionSummary.impressionCount} ${t("common.impressions")}`}
{`${elementSummary.impressionCount} ${t("common.impressions")}`}
</div>
<div className="flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{`${questionSummary.clickCount} ${t("common.clicks")}`}
{`${elementSummary.clickCount} ${t("common.clicks")}`}
</div>
{!questionSummary.question.required && (
{!elementSummary.element.required && (
<div className="flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{`${questionSummary.skipCount} ${t("common.skips")}`}
{`${elementSummary.skipCount} ${t("common.skips")}`}
</div>
)}
</>
@@ -46,16 +46,16 @@ export const CTASummary = ({ questionSummary, survey }: CTASummaryProps) => {
<p className="font-semibold text-slate-700">CTR</p>
<div>
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
{convertFloatToNDecimal(questionSummary.ctr.percentage, 2)}%
{convertFloatToNDecimal(elementSummary.ctr.percentage, 2)}%
</p>
</div>
</div>
<p className="flex w-32 items-end justify-end text-slate-600">
{questionSummary.ctr.count}{" "}
{questionSummary.ctr.count === 1 ? t("common.click") : t("common.clicks")}
{elementSummary.ctr.count}{" "}
{elementSummary.ctr.count === 1 ? t("common.click") : t("common.clicks")}
</p>
</div>
<ProgressBar barColor="bg-brand-dark" progress={questionSummary.ctr.percentage / 100} />
<ProgressBar barColor="bg-brand-dark" progress={elementSummary.ctr.percentage / 100} />
</div>
</div>
);

View File

@@ -4,20 +4,20 @@ import { useTranslation } from "react-i18next";
import { TSurvey, TSurveyElementSummaryCal } from "@formbricks/types/surveys/types";
import { convertFloatToNDecimal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
import { ProgressBar } from "@/modules/ui/components/progress-bar";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
import { ElementSummaryHeader } from "./ElementSummaryHeader";
interface CalSummaryProps {
questionSummary: TSurveyElementSummaryCal;
elementSummary: TSurveyElementSummaryCal;
environmentId: string;
survey: TSurvey;
}
export const CalSummary = ({ questionSummary, survey }: CalSummaryProps) => {
export const CalSummary = ({ elementSummary, survey }: CalSummaryProps) => {
const { t } = useTranslation();
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
<div>
<div className="text flex justify-between px-2 pb-2">
@@ -25,16 +25,16 @@ export const CalSummary = ({ questionSummary, survey }: CalSummaryProps) => {
<p className="font-semibold text-slate-700">{t("common.booked")}</p>
<div>
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
{convertFloatToNDecimal(questionSummary.booked.percentage, 2)}%
{convertFloatToNDecimal(elementSummary.booked.percentage, 2)}%
</p>
</div>
</div>
<p className="flex w-32 items-end justify-end text-slate-600">
{questionSummary.booked.count}{" "}
{questionSummary.booked.count === 1 ? t("common.response") : t("common.responses")}
{elementSummary.booked.count}{" "}
{elementSummary.booked.count === 1 ? t("common.response") : t("common.responses")}
</p>
</div>
<ProgressBar barColor="bg-brand-dark" progress={questionSummary.booked.percentage / 100} />
<ProgressBar barColor="bg-brand-dark" progress={elementSummary.booked.percentage / 100} />
</div>
<div>
<div className="text flex justify-between px-2 pb-2">
@@ -42,16 +42,16 @@ export const CalSummary = ({ questionSummary, survey }: CalSummaryProps) => {
<p className="font-semibold text-slate-700">{t("common.dismissed")}</p>
<div>
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
{convertFloatToNDecimal(questionSummary.skipped.percentage, 2)}%
{convertFloatToNDecimal(elementSummary.skipped.percentage, 2)}%
</p>
</div>
</div>
<p className="flex w-32 items-end justify-end text-slate-600">
{questionSummary.skipped.count}{" "}
{questionSummary.skipped.count === 1 ? t("common.response") : t("common.responses")}
{elementSummary.skipped.count}{" "}
{elementSummary.skipped.count === 1 ? t("common.response") : t("common.responses")}
</p>
</div>
<ProgressBar barColor="bg-brand-dark" progress={questionSummary.skipped.percentage / 100} />
<ProgressBar barColor="bg-brand-dark" progress={elementSummary.skipped.percentage / 100} />
</div>
</div>
</div>

View File

@@ -3,40 +3,40 @@
import { useTranslation } from "react-i18next";
import { type TI18nString } from "@formbricks/types/i18n";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { TSurvey, TSurveyElementSummaryConsent, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { TSurvey, TSurveyElementSummaryConsent } from "@formbricks/types/surveys/types";
import { ProgressBar } from "@/modules/ui/components/progress-bar";
import { convertFloatToNDecimal } from "../lib/utils";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
import { ElementSummaryHeader } from "./ElementSummaryHeader";
interface ConsentSummaryProps {
questionSummary: TSurveyElementSummaryConsent;
elementSummary: TSurveyElementSummaryConsent;
survey: TSurvey;
setFilter: (
questionId: TSurveyQuestionId,
elementId: string,
label: TI18nString,
questionType: TSurveyElementTypeEnum,
elementType: TSurveyElementTypeEnum,
filterValue: string,
filterComboBoxValue?: string | string[]
) => void;
}
export const ConsentSummary = ({ questionSummary, survey, setFilter }: ConsentSummaryProps) => {
export const ConsentSummary = ({ elementSummary, survey, setFilter }: ConsentSummaryProps) => {
const { t } = useTranslation();
const summaryItems = [
{
title: t("common.accepted"),
percentage: questionSummary.accepted.percentage,
count: questionSummary.accepted.count,
percentage: elementSummary.accepted.percentage,
count: elementSummary.accepted.count,
},
{
title: t("common.dismissed"),
percentage: questionSummary.dismissed.percentage,
count: questionSummary.dismissed.count,
percentage: elementSummary.dismissed.percentage,
count: elementSummary.dismissed.count,
},
];
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
{summaryItems.map((summaryItem) => {
return (
@@ -45,9 +45,9 @@ export const ConsentSummary = ({ questionSummary, survey, setFilter }: ConsentSu
key={summaryItem.title}
onClick={() =>
setFilter(
questionSummary.question.id,
questionSummary.question.headline,
questionSummary.question.type,
elementSummary.element.id,
elementSummary.element.headline,
elementSummary.element.type,
"is",
summaryItem.title
)

View File

@@ -8,17 +8,17 @@ import { timeSince } from "@/lib/time";
import { getContactIdentifier } from "@/lib/utils/contact";
import { ArrayResponse } from "@/modules/ui/components/array-response";
import { PersonAvatar } from "@/modules/ui/components/avatars";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
import { ElementSummaryHeader } from "./ElementSummaryHeader";
interface ContactInfoSummaryProps {
questionSummary: TSurveyElementSummaryContactInfo;
elementSummary: TSurveyElementSummaryContactInfo;
environmentId: string;
survey: TSurvey;
locale: TUserLocale;
}
export const ContactInfoSummary = ({
questionSummary,
elementSummary,
environmentId,
survey,
locale,
@@ -26,7 +26,7 @@ export const ContactInfoSummary = ({
const { t } = useTranslation();
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
<div>
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">
<div className="pl-4 md:pl-6">{t("common.user")}</div>
@@ -34,7 +34,7 @@ export const ContactInfoSummary = ({
<div className="px-4 md:px-6">{t("common.time")}</div>
</div>
<div className="max-h-[62vh] w-full overflow-y-auto">
{questionSummary.samples.map((response) => {
{elementSummary.samples.map((response) => {
return (
<div
key={response.id}

View File

@@ -10,28 +10,23 @@ import { getContactIdentifier } from "@/lib/utils/contact";
import { formatDateWithOrdinal } from "@/lib/utils/datetime";
import { PersonAvatar } from "@/modules/ui/components/avatars";
import { Button } from "@/modules/ui/components/button";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
import { ElementSummaryHeader } from "./ElementSummaryHeader";
interface DateQuestionSummary {
questionSummary: TSurveyElementSummaryDate;
interface DateElementSummary {
elementSummary: TSurveyElementSummaryDate;
environmentId: string;
survey: TSurvey;
locale: TUserLocale;
}
export const DateQuestionSummary = ({
questionSummary,
environmentId,
survey,
locale,
}: DateQuestionSummary) => {
export const DateElementSummary = ({ elementSummary, environmentId, survey, locale }: DateElementSummary) => {
const { t } = useTranslation();
const [visibleResponses, setVisibleResponses] = useState(10);
const handleLoadMore = () => {
// Increase the number of visible responses by 10, not exceeding the total number of responses
setVisibleResponses((prevVisibleResponses) =>
Math.min(prevVisibleResponses + 10, questionSummary.samples.length)
Math.min(prevVisibleResponses + 10, elementSummary.samples.length)
);
};
@@ -47,7 +42,7 @@ export const DateQuestionSummary = ({
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
<div className="">
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">
<div className="pl-4 md:pl-6">{t("common.user")}</div>
@@ -55,7 +50,7 @@ export const DateQuestionSummary = ({
<div className="px-4 md:px-6">{t("common.time")}</div>
</div>
<div className="max-h-[62vh] w-full overflow-y-auto">
{questionSummary.samples.slice(0, visibleResponses).map((response) => (
{elementSummary.samples.slice(0, visibleResponses).map((response) => (
<div
key={response.id}
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 last:border-transparent md:text-base">
@@ -89,7 +84,7 @@ export const DateQuestionSummary = ({
</div>
))}
</div>
{visibleResponses < questionSummary.samples.length && (
{visibleResponses < elementSummary.samples.length && (
<div className="flex justify-center py-4">
<Button onClick={handleLoadMore} variant="secondary" size="sm">
{t("common.load_more")}

View File

@@ -7,24 +7,24 @@ import { TSurvey, TSurveyElementSummary } from "@formbricks/types/surveys/types"
import { getTextContent } from "@formbricks/types/surveys/validation";
import { recallToHeadline } from "@/lib/utils/recall";
import { formatTextWithSlashes } from "@/modules/survey/editor/lib/utils";
import { getQuestionTypes } from "@/modules/survey/lib/questions";
import { getElementTypes } from "@/modules/survey/lib/elements";
import { IdBadge } from "@/modules/ui/components/id-badge";
interface HeadProps {
questionSummary: TSurveyElementSummary;
elementSummary: TSurveyElementSummary;
showResponses?: boolean;
additionalInfo?: JSX.Element;
survey: TSurvey;
}
export const QuestionSummaryHeader = ({
questionSummary,
export const ElementSummaryHeader = ({
elementSummary,
additionalInfo,
showResponses = true,
survey,
}: HeadProps) => {
const { t } = useTranslation();
const questionType = getQuestionTypes(t).find((type) => type.id === questionSummary.question.type);
const elementType = getElementTypes(t).find((type) => type.id === elementSummary.element.type);
return (
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
@@ -32,7 +32,7 @@ export const QuestionSummaryHeader = ({
<h3 className="pb-1 text-lg font-semibold text-slate-900 md:text-xl">
{formatTextWithSlashes(
getTextContent(
recallToHeadline(questionSummary.question.headline, survey, true, "default")["default"]
recallToHeadline(elementSummary.element.headline, survey, true, "default")["default"]
),
"@",
["text-lg"]
@@ -41,24 +41,24 @@ export const QuestionSummaryHeader = ({
</div>
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
<div className="flex items-center rounded-lg bg-slate-100 p-2">
{questionType && <questionType.icon className="mr-2 h-4 w-4" />}
{questionType ? questionType.label : t("environments.surveys.summary.unknown_question_type")}{" "}
{elementType && <elementType.icon className="mr-2 h-4 w-4" />}
{elementType ? elementType.label : t("environments.surveys.summary.unknown_question_type")}{" "}
{t("common.question")}
</div>
{showResponses && (
<div className="flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{`${questionSummary.responseCount} ${t("common.responses")}`}
{`${elementSummary.responseCount} ${t("common.responses")}`}
</div>
)}
{additionalInfo}
{!questionSummary.question.required && (
{!elementSummary.element.required && (
<div className="flex items-center rounded-lg bg-slate-100 p-2">
{t("environments.surveys.edit.optional")}
</div>
)}
</div>
<IdBadge id={questionSummary.question.id} label={t("common.question_id")} />
<IdBadge id={elementSummary.element.id} label={t("common.question_id")} />
</div>
);
};

View File

@@ -11,17 +11,17 @@ import { getContactIdentifier } from "@/lib/utils/contact";
import { getOriginalFileNameFromUrl } from "@/modules/storage/utils";
import { PersonAvatar } from "@/modules/ui/components/avatars";
import { Button } from "@/modules/ui/components/button";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
import { ElementSummaryHeader } from "./ElementSummaryHeader";
interface FileUploadSummaryProps {
questionSummary: TSurveyElementSummaryFileUpload;
elementSummary: TSurveyElementSummaryFileUpload;
environmentId: string;
survey: TSurvey;
locale: TUserLocale;
}
export const FileUploadSummary = ({
questionSummary,
elementSummary,
environmentId,
survey,
locale,
@@ -31,13 +31,13 @@ export const FileUploadSummary = ({
const handleLoadMore = () => {
// Increase the number of visible responses by 10, not exceeding the total number of responses
setVisibleResponses((prevVisibleResponses) =>
Math.min(prevVisibleResponses + 10, questionSummary.files.length)
Math.min(prevVisibleResponses + 10, elementSummary.files.length)
);
};
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
<div className="">
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">
<div className="pl-4 md:pl-6">{t("common.user")}</div>
@@ -45,7 +45,7 @@ export const FileUploadSummary = ({
<div className="px-4 md:px-6">{t("common.time")}</div>
</div>
<div className="max-h-[62vh] w-full overflow-y-auto">
{questionSummary.files.slice(0, visibleResponses).map((response) => (
{elementSummary.files.slice(0, visibleResponses).map((response) => (
<div
key={response.id}
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 last:border-transparent md:text-base">
@@ -109,7 +109,7 @@ export const FileUploadSummary = ({
</div>
))}
</div>
{visibleResponses < questionSummary.files.length && (
{visibleResponses < elementSummary.files.length && (
<div className="flex justify-center py-4">
<Button onClick={handleLoadMore} variant="secondary" size="sm">
{t("common.load_more")}

View File

@@ -13,24 +13,24 @@ import { Button } from "@/modules/ui/components/button";
interface HiddenFieldsSummaryProps {
environment: TEnvironment;
questionSummary: TSurveyElementSummaryHiddenFields;
elementSummary: TSurveyElementSummaryHiddenFields;
locale: TUserLocale;
}
export const HiddenFieldsSummary = ({ environment, questionSummary, locale }: HiddenFieldsSummaryProps) => {
export const HiddenFieldsSummary = ({ environment, elementSummary, locale }: HiddenFieldsSummaryProps) => {
const [visibleResponses, setVisibleResponses] = useState(10);
const { t } = useTranslation();
const handleLoadMore = () => {
// Increase the number of visible responses by 10, not exceeding the total number of responses
setVisibleResponses((prevVisibleResponses) =>
Math.min(prevVisibleResponses + 10, questionSummary.samples.length)
Math.min(prevVisibleResponses + 10, elementSummary.samples.length)
);
};
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
<div className={"align-center flex justify-between gap-4"}>
<h3 className="pb-1 text-lg font-semibold text-slate-900 md:text-xl">{questionSummary.id}</h3>
<h3 className="pb-1 text-lg font-semibold text-slate-900 md:text-xl">{elementSummary.id}</h3>
</div>
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
@@ -40,8 +40,8 @@ export const HiddenFieldsSummary = ({ environment, questionSummary, locale }: Hi
</div>
<div className="flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{questionSummary.responseCount}{" "}
{questionSummary.responseCount === 1 ? t("common.response") : t("common.responses")}
{elementSummary.responseCount}{" "}
{elementSummary.responseCount === 1 ? t("common.response") : t("common.responses")}
</div>
</div>
</div>
@@ -51,7 +51,7 @@ export const HiddenFieldsSummary = ({ environment, questionSummary, locale }: Hi
<div className="col-span-2 pl-4 md:pl-6">{t("common.response")}</div>
<div className="px-4 md:px-6">{t("common.time")}</div>
</div>
{questionSummary.samples.slice(0, visibleResponses).map((response, idx) => (
{elementSummary.samples.slice(0, visibleResponses).map((response, idx) => (
<div
key={`${response.value}-${idx}`}
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
@@ -84,7 +84,7 @@ export const HiddenFieldsSummary = ({ environment, questionSummary, locale }: Hi
</div>
</div>
))}
{visibleResponses < questionSummary.samples.length && (
{visibleResponses < elementSummary.samples.length && (
<div className="flex justify-center py-4">
<Button onClick={handleLoadMore} variant="secondary" size="sm">
{t("common.load_more")}

View File

@@ -3,23 +3,23 @@
import { useTranslation } from "react-i18next";
import { type TI18nString } from "@formbricks/types/i18n";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { TSurvey, TSurveyElementSummaryMatrix, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { TSurvey, TSurveyElementSummaryMatrix } from "@formbricks/types/surveys/types";
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
import { ElementSummaryHeader } from "./ElementSummaryHeader";
interface MatrixQuestionSummaryProps {
questionSummary: TSurveyElementSummaryMatrix;
interface MatrixElementSummaryProps {
elementSummary: TSurveyElementSummaryMatrix;
survey: TSurvey;
setFilter: (
questionId: TSurveyQuestionId,
elementId: string,
label: TI18nString,
questionType: TSurveyElementTypeEnum,
elementType: TSurveyElementTypeEnum,
filterValue: string,
filterComboBoxValue?: string | string[]
) => void;
}
export const MatrixQuestionSummary = ({ questionSummary, survey, setFilter }: MatrixQuestionSummaryProps) => {
export const MatrixElementSummary = ({ elementSummary, survey, setFilter }: MatrixElementSummaryProps) => {
const { t } = useTranslation();
const getOpacityLevel = (percentage: number): string => {
const parsedPercentage = percentage;
@@ -36,13 +36,11 @@ export const MatrixQuestionSummary = ({ questionSummary, survey, setFilter }: Ma
return "";
};
const columns = questionSummary.data[0]
? questionSummary.data[0].columnPercentages.map((c) => c.column)
: [];
const columns = elementSummary.data[0] ? elementSummary.data[0].columnPercentages.map((c) => c.column) : [];
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
<div className="overflow-x-auto p-6">
{/* Summary Table */}
<table className="mx-auto border-collapse cursor-default text-left">
@@ -59,7 +57,7 @@ export const MatrixQuestionSummary = ({ questionSummary, survey, setFilter }: Ma
</tr>
</thead>
<tbody>
{questionSummary.data.map(({ rowLabel, columnPercentages }, rowIndex) => (
{elementSummary.data.map(({ rowLabel, columnPercentages }, rowIndex) => (
<tr key={rowLabel}>
<td className="max-w-60 overflow-hidden text-ellipsis whitespace-nowrap p-4">
<TooltipRenderer tooltipContent={getTooltipContent(rowLabel)} shouldRender={true}>
@@ -75,16 +73,16 @@ export const MatrixQuestionSummary = ({ questionSummary, survey, setFilter }: Ma
tooltipContent={getTooltipContent(
undefined,
percentage,
questionSummary.data[rowIndex].totalResponsesForRow
elementSummary.data[rowIndex].totalResponsesForRow
)}>
<button
style={{ backgroundColor: `rgba(0,196,184,${getOpacityLevel(percentage)})` }}
className="hover:outline-brand-dark m-1 flex h-full w-40 cursor-pointer items-center justify-center rounded p-4 text-sm text-slate-950 hover:outline"
onClick={() =>
setFilter(
questionSummary.question.id,
questionSummary.question.headline,
questionSummary.question.type,
elementSummary.element.id,
elementSummary.element.headline,
elementSummary.element.type,
rowLabel,
column
)

View File

@@ -6,12 +6,7 @@ import { Fragment, useState } from "react";
import { useTranslation } from "react-i18next";
import { TI18nString } from "@formbricks/types/i18n";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import {
TSurvey,
TSurveyElementSummaryMultipleChoice,
TSurveyQuestionId,
TSurveyType,
} from "@formbricks/types/surveys/types";
import { TSurvey, TSurveyElementSummaryMultipleChoice, TSurveyType } from "@formbricks/types/surveys/types";
import { getChoiceIdByValue } from "@/lib/response/utils";
import { getContactIdentifier } from "@/lib/utils/contact";
import { PersonAvatar } from "@/modules/ui/components/avatars";
@@ -19,24 +14,24 @@ import { Button } from "@/modules/ui/components/button";
import { IdBadge } from "@/modules/ui/components/id-badge";
import { ProgressBar } from "@/modules/ui/components/progress-bar";
import { convertFloatToNDecimal } from "../lib/utils";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
import { ElementSummaryHeader } from "./ElementSummaryHeader";
interface MultipleChoiceSummaryProps {
questionSummary: TSurveyElementSummaryMultipleChoice;
elementSummary: TSurveyElementSummaryMultipleChoice;
environmentId: string;
surveyType: TSurveyType;
survey: TSurvey;
setFilter: (
questionId: TSurveyQuestionId,
elementId: string,
label: TI18nString,
questionType: TSurveyElementTypeEnum,
elementType: TSurveyElementTypeEnum,
filterValue: string,
filterComboBoxValue?: string | string[]
) => void;
}
export const MultipleChoiceSummary = ({
questionSummary,
elementSummary,
environmentId,
surveyType,
survey,
@@ -44,9 +39,9 @@ export const MultipleChoiceSummary = ({
}: MultipleChoiceSummaryProps) => {
const { t } = useTranslation();
const [visibleOtherResponses, setVisibleOtherResponses] = useState(10);
const otherValue = questionSummary.question.choices.find((choice) => choice.id === "other")?.label.default;
const otherValue = elementSummary.element.choices.find((choice) => choice.id === "other")?.label.default;
// sort by count and transform to array
const results = Object.values(questionSummary.choices).sort((a, b) => {
const results = Object.values(elementSummary.choices).sort((a, b) => {
const aHasOthers = (a.others?.length ?? 0) > 0;
const bHasOthers = (b.others?.length ?? 0) > 0;
@@ -73,21 +68,21 @@ export const MultipleChoiceSummary = ({
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader
questionSummary={questionSummary}
<ElementSummaryHeader
elementSummary={elementSummary}
survey={survey}
additionalInfo={
questionSummary.type === "multipleChoiceMulti" ? (
elementSummary.type === "multipleChoiceMulti" ? (
<div className="flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{`${questionSummary.selectionCount} ${t("common.selections")}`}
{`${elementSummary.selectionCount} ${t("common.selections")}`}
</div>
) : undefined
}
/>
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
{results.map((result) => {
const choiceId = getChoiceIdByValue(result.value, questionSummary.question);
const choiceId = getChoiceIdByValue(result.value, elementSummary.element);
return (
<Fragment key={result.value}>
<button
@@ -95,10 +90,10 @@ export const MultipleChoiceSummary = ({
className="group w-full cursor-pointer"
onClick={() =>
setFilter(
questionSummary.question.id,
questionSummary.question.headline,
questionSummary.question.type,
questionSummary.type === "multipleChoiceSingle" || otherValue === result.value
elementSummary.element.id,
elementSummary.element.headline,
elementSummary.element.type,
elementSummary.type === "multipleChoiceSingle" || otherValue === result.value
? t("environments.surveys.summary.includes_either")
: t("environments.surveys.summary.includes_all"),
[result.value]

View File

@@ -3,24 +3,24 @@
import { useTranslation } from "react-i18next";
import { type TI18nString } from "@formbricks/types/i18n";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { TSurvey, TSurveyElementSummaryNps, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { TSurvey, TSurveyElementSummaryNps } from "@formbricks/types/surveys/types";
import { HalfCircle, ProgressBar } from "@/modules/ui/components/progress-bar";
import { convertFloatToNDecimal } from "../lib/utils";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
import { ElementSummaryHeader } from "./ElementSummaryHeader";
interface NPSSummaryProps {
questionSummary: TSurveyElementSummaryNps;
elementSummary: TSurveyElementSummaryNps;
survey: TSurvey;
setFilter: (
questionId: TSurveyQuestionId,
elementId: string,
label: TI18nString,
questionType: TSurveyElementTypeEnum,
elementType: TSurveyElementTypeEnum,
filterValue: string,
filterComboBoxValue?: string | string[]
) => void;
}
export const NPSSummary = ({ questionSummary, survey, setFilter }: NPSSummaryProps) => {
export const NPSSummary = ({ elementSummary, survey, setFilter }: NPSSummaryProps) => {
const { t } = useTranslation();
const applyFilter = (group: string) => {
const filters = {
@@ -46,9 +46,9 @@ export const NPSSummary = ({ questionSummary, survey, setFilter }: NPSSummaryPro
if (filter) {
setFilter(
questionSummary.question.id,
questionSummary.question.headline,
questionSummary.question.type,
elementSummary.element.id,
elementSummary.element.headline,
elementSummary.element.type,
filter.comparison,
filter.values
);
@@ -57,7 +57,7 @@ export const NPSSummary = ({ questionSummary, survey, setFilter }: NPSSummaryPro
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
{["promoters", "passives", "detractors", "dismissed"].map((group) => (
<button
@@ -73,25 +73,25 @@ export const NPSSummary = ({ questionSummary, survey, setFilter }: NPSSummaryPro
</p>
<div>
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
{convertFloatToNDecimal(questionSummary[group]?.percentage, 2)}%
{convertFloatToNDecimal(elementSummary[group]?.percentage, 2)}%
</p>
</div>
</div>
<p className="flex w-32 items-end justify-end text-slate-600">
{questionSummary[group]?.count}{" "}
{questionSummary[group]?.count === 1 ? t("common.response") : t("common.responses")}
{elementSummary[group]?.count}{" "}
{elementSummary[group]?.count === 1 ? t("common.response") : t("common.responses")}
</p>
</div>
<ProgressBar
barColor={group === "dismissed" ? "bg-slate-600" : "bg-brand-dark"}
progress={questionSummary[group]?.percentage / 100}
progress={elementSummary[group]?.percentage / 100}
/>
</button>
))}
</div>
<div className="flex justify-center pb-4 pt-4">
<HalfCircle value={questionSummary.score} />
<HalfCircle value={elementSummary.score} />
</div>
</div>
);

View File

@@ -11,29 +11,29 @@ import { renderHyperlinkedContent } from "@/modules/analysis/utils";
import { PersonAvatar } from "@/modules/ui/components/avatars";
import { Button } from "@/modules/ui/components/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/modules/ui/components/table";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
import { ElementSummaryHeader } from "./ElementSummaryHeader";
interface OpenTextSummaryProps {
questionSummary: TSurveyElementSummaryOpenText;
elementSummary: TSurveyElementSummaryOpenText;
environmentId: string;
survey: TSurvey;
locale: TUserLocale;
}
export const OpenTextSummary = ({ questionSummary, environmentId, survey, locale }: OpenTextSummaryProps) => {
export const OpenTextSummary = ({ elementSummary, environmentId, survey, locale }: OpenTextSummaryProps) => {
const { t } = useTranslation();
const [visibleResponses, setVisibleResponses] = useState(10);
const handleLoadMore = () => {
// Increase the number of visible responses by 10, not exceeding the total number of responses
setVisibleResponses((prevVisibleResponses) =>
Math.min(prevVisibleResponses + 10, questionSummary.samples.length)
Math.min(prevVisibleResponses + 10, elementSummary.samples.length)
);
};
return (
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
<div className="border-t border-slate-200"></div>
<div className="max-h-[40vh] overflow-y-auto">
<Table>
@@ -45,7 +45,7 @@ export const OpenTextSummary = ({ questionSummary, environmentId, survey, locale
</TableRow>
</TableHeader>
<TableBody>
{questionSummary.samples.slice(0, visibleResponses).map((response) => (
{elementSummary.samples.slice(0, visibleResponses).map((response) => (
<TableRow key={response.id}>
<TableCell>
{response.contact ? (
@@ -80,7 +80,7 @@ export const OpenTextSummary = ({ questionSummary, environmentId, survey, locale
))}
</TableBody>
</Table>
{visibleResponses < questionSummary.samples.length && (
{visibleResponses < elementSummary.samples.length && (
<div className="flex justify-center py-4">
<Button onClick={handleLoadMore} variant="secondary" size="sm">
{t("common.load_more")}

View File

@@ -5,50 +5,46 @@ import Image from "next/image";
import { useTranslation } from "react-i18next";
import { type TI18nString } from "@formbricks/types/i18n";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import {
TSurvey,
TSurveyElementSummaryPictureSelection,
TSurveyQuestionId,
} from "@formbricks/types/surveys/types";
import { TSurvey, TSurveyElementSummaryPictureSelection } from "@formbricks/types/surveys/types";
import { getChoiceIdByValue } from "@/lib/response/utils";
import { IdBadge } from "@/modules/ui/components/id-badge";
import { ProgressBar } from "@/modules/ui/components/progress-bar";
import { convertFloatToNDecimal } from "../lib/utils";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
import { ElementSummaryHeader } from "./ElementSummaryHeader";
interface PictureChoiceSummaryProps {
questionSummary: TSurveyElementSummaryPictureSelection;
elementSummary: TSurveyElementSummaryPictureSelection;
survey: TSurvey;
setFilter: (
questionId: TSurveyQuestionId,
elementId: string,
label: TI18nString,
questionType: TSurveyElementTypeEnum,
elementType: TSurveyElementTypeEnum,
filterValue: string,
filterComboBoxValue?: string | string[]
) => void;
}
export const PictureChoiceSummary = ({ questionSummary, survey, setFilter }: PictureChoiceSummaryProps) => {
const results = questionSummary.choices;
export const PictureChoiceSummary = ({ elementSummary, survey, setFilter }: PictureChoiceSummaryProps) => {
const results = elementSummary.choices;
const { t } = useTranslation();
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader
questionSummary={questionSummary}
<ElementSummaryHeader
elementSummary={elementSummary}
survey={survey}
additionalInfo={
questionSummary.question.allowMulti ? (
elementSummary.element.allowMulti ? (
<div className="flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{`${questionSummary.selectionCount} ${t("common.selections")}`}
{`${elementSummary.selectionCount} ${t("common.selections")}`}
</div>
) : undefined
}
/>
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
{results.map((result, index) => {
const choiceId = getChoiceIdByValue(result.imageUrl, questionSummary.question);
const choiceId = getChoiceIdByValue(result.imageUrl, elementSummary.element);
return (
<button
type="button"
@@ -56,9 +52,9 @@ export const PictureChoiceSummary = ({ questionSummary, survey, setFilter }: Pic
key={result.id}
onClick={() =>
setFilter(
questionSummary.question.id,
questionSummary.question.headline,
questionSummary.question.type,
elementSummary.element.id,
elementSummary.element.headline,
elementSummary.element.type,
t("environments.surveys.summary.includes_all"),
[`${t("environments.surveys.edit.picture_idx", { idx: index + 1 })}`]
)

View File

@@ -3,26 +3,26 @@ import { TSurvey, TSurveyElementSummaryRanking } from "@formbricks/types/surveys
import { getChoiceIdByValue } from "@/lib/response/utils";
import { IdBadge } from "@/modules/ui/components/id-badge";
import { convertFloatToNDecimal } from "../lib/utils";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
import { ElementSummaryHeader } from "./ElementSummaryHeader";
interface RankingSummaryProps {
questionSummary: TSurveyElementSummaryRanking;
elementSummary: TSurveyElementSummaryRanking;
survey: TSurvey;
}
export const RankingSummary = ({ questionSummary, survey }: RankingSummaryProps) => {
export const RankingSummary = ({ elementSummary, survey }: RankingSummaryProps) => {
// sort by count and transform to array
const { t } = useTranslation();
const results = Object.values(questionSummary.choices).sort((a, b) => {
const results = Object.values(elementSummary.choices).sort((a, b) => {
return a.avgRanking - b.avgRanking; // Sort by count
});
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} />
<ElementSummaryHeader elementSummary={elementSummary} survey={survey} />
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
{results.map((result, resultsIdx) => {
const choiceId = getChoiceIdByValue(result.value, questionSummary.question);
const choiceId = getChoiceIdByValue(result.value, elementSummary.element);
return (
<div key={result.value} className="group cursor-pointer">
<div className="text flex flex-col justify-between px-2 pb-2 sm:flex-row">

View File

@@ -5,57 +5,57 @@ import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { type TI18nString } from "@formbricks/types/i18n";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { TSurvey, TSurveyElementSummaryRating, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { TSurvey, TSurveyElementSummaryRating } from "@formbricks/types/surveys/types";
import { convertFloatToNDecimal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
import { ProgressBar } from "@/modules/ui/components/progress-bar";
import { RatingResponse } from "@/modules/ui/components/rating-response";
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
import { ElementSummaryHeader } from "./ElementSummaryHeader";
interface RatingSummaryProps {
questionSummary: TSurveyElementSummaryRating;
elementSummary: TSurveyElementSummaryRating;
survey: TSurvey;
setFilter: (
questionId: TSurveyQuestionId,
elementId: string,
label: TI18nString,
questionType: TSurveyElementTypeEnum,
elementType: TSurveyElementTypeEnum,
filterValue: string,
filterComboBoxValue?: string | string[]
) => void;
}
export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSummaryProps) => {
export const RatingSummary = ({ elementSummary, survey, setFilter }: RatingSummaryProps) => {
const { t } = useTranslation();
const getIconBasedOnScale = useMemo(() => {
const scale = questionSummary.question.scale;
const scale = elementSummary.element.scale;
if (scale === "number") return <CircleSlash2 className="h-4 w-4" />;
else if (scale === "star") return <StarIcon fill="rgb(250 204 21)" className="h-4 w-4 text-yellow-400" />;
else if (scale === "smiley") return <SmileIcon className="h-4 w-4" />;
}, [questionSummary]);
}, [elementSummary]);
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<QuestionSummaryHeader
questionSummary={questionSummary}
<ElementSummaryHeader
elementSummary={elementSummary}
survey={survey}
additionalInfo={
<div className="flex items-center space-x-2 rounded-lg bg-slate-100 p-2">
{getIconBasedOnScale}
<div>
{t("environments.surveys.summary.overall")}: {questionSummary.average.toFixed(2)}
{t("environments.surveys.summary.overall")}: {elementSummary.average.toFixed(2)}
</div>
</div>
}
/>
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
{questionSummary.choices.map((result) => (
{elementSummary.choices.map((result) => (
<button
className="w-full cursor-pointer hover:opacity-80"
key={result.rating}
onClick={() =>
setFilter(
questionSummary.question.id,
questionSummary.question.headline,
questionSummary.question.type,
elementSummary.element.id,
elementSummary.element.headline,
elementSummary.element.type,
t("environments.surveys.summary.is_equal_to"),
result.rating.toString()
)
@@ -64,10 +64,10 @@ export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSumm
<div className="mr-8 flex items-center space-x-1">
<div className="font-semibold text-slate-700">
<RatingResponse
scale={questionSummary.question.scale}
scale={elementSummary.element.scale}
answer={result.rating}
range={questionSummary.question.range}
addColors={questionSummary.question.isColorCodingEnabled}
range={elementSummary.element.range}
addColors={elementSummary.element.isColorCodingEnabled}
/>
</div>
<div>
@@ -84,14 +84,14 @@ export const RatingSummary = ({ questionSummary, survey, setFilter }: RatingSumm
</button>
))}
</div>
{questionSummary.dismissed && questionSummary.dismissed.count > 0 && (
{elementSummary.dismissed && elementSummary.dismissed.count > 0 && (
<div className="rounded-b-lg border-t bg-white px-6 py-4">
<div key="dismissed">
<div className="text flex justify-between px-2">
<p className="font-semibold text-slate-700">{t("common.dismissed")}</p>
<p className="flex w-32 items-end justify-end text-slate-600">
{questionSummary.dismissed.count}{" "}
{questionSummary.dismissed.count === 1 ? t("common.response") : t("common.responses")}
{elementSummary.dismissed.count}{" "}
{elementSummary.dismissed.count === 1 ? t("common.response") : t("common.responses")}
</p>
</div>
</div>

View File

@@ -6,7 +6,7 @@ import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { TSurvey, TSurveySummary } from "@formbricks/types/surveys/types";
import { recallToHeadline } from "@/lib/utils/recall";
import { formatTextWithSlashes } from "@/modules/survey/editor/lib/utils";
import { getQuestionIcon } from "@/modules/survey/lib/questions";
import { getElementIcon } from "@/modules/survey/lib/elements";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
interface SummaryDropOffsProps {
@@ -16,8 +16,8 @@ interface SummaryDropOffsProps {
export const SummaryDropOffs = ({ dropOff, survey }: SummaryDropOffsProps) => {
const { t } = useTranslation();
const getIcon = (questionType: TSurveyElementTypeEnum) => {
const Icon = getQuestionIcon(questionType, t);
const getIcon = (elementType: TSurveyElementTypeEnum) => {
const Icon = getElementIcon(elementType, t);
return <Icon className="mt-[3px] h-5 w-5 shrink-0 text-slate-600" />;
};
@@ -45,10 +45,10 @@ export const SummaryDropOffs = ({ dropOff, survey }: SummaryDropOffsProps) => {
</div>
{dropOff.map((quesDropOff) => (
<div
key={quesDropOff.questionId}
key={quesDropOff.elementId}
className="grid grid-cols-6 items-start border-b border-slate-100 text-xs text-slate-800 md:text-sm">
<div className="col-span-3 flex gap-3 px-4 py-2 md:px-6">
{getIcon(quesDropOff.questionType)}
{getIcon(quesDropOff.elementType)}
<p>
{formatTextWithSlashes(
recallToHeadline(

View File

@@ -17,10 +17,10 @@ import { CTASummary } from "@/app/(app)/environments/[environmentId]/surveys/[su
import { CalSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary";
import { ConsentSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary";
import { ContactInfoSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ContactInfoSummary";
import { DateQuestionSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateQuestionSummary";
import { DateElementSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/DateElementSummary";
import { FileUploadSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/FileUploadSummary";
import { HiddenFieldsSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary";
import { MatrixQuestionSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixQuestionSummary";
import { MatrixElementSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MatrixElementSummary";
import { MultipleChoiceSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary";
import { NPSSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/NPSSummary";
import { OpenTextSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary";
@@ -28,7 +28,7 @@ import { PictureChoiceSummary } from "@/app/(app)/environments/[environmentId]/s
import { RankingSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RankingSummary";
import { RatingSummary } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/RatingSummary";
import { constructToastMessage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ElementsComboBox";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
import { SkeletonLoader } from "@/modules/ui/components/skeleton-loader";
@@ -46,29 +46,29 @@ export const SummaryList = ({ summary, environment, responseCount, survey, local
const { setSelectedFilter, selectedFilter } = useResponseFilter();
const { t } = useTranslation();
const setFilter = (
questionId: string,
elementId: string,
label: TI18nString,
questionType: TSurveyElementTypeEnum,
elementType: TSurveyElementTypeEnum,
filterValue: string,
filterComboBoxValue?: string | string[]
) => {
const filterObject: SelectedFilterValue = { ...selectedFilter };
const value = {
id: questionId,
id: elementId,
label: getLocalizedValue(label, "default"),
questionType: questionType,
type: OptionsType.QUESTIONS,
elementType,
type: OptionsType.ELEMENTS,
};
// Find the index of the existing filter with the same questionId
// Find the index of the existing filter with the same elementId
const existingFilterIndex = filterObject.filter.findIndex(
(filter) => filter.questionType.id === questionId
(filter) => filter.elementType.id === elementId
);
if (existingFilterIndex !== -1) {
// Replace the existing filter
filterObject.filter[existingFilterIndex] = {
questionType: value,
elementType: value,
filterType: {
filterComboBoxValue: filterComboBoxValue,
filterValue: filterValue,
@@ -78,14 +78,14 @@ export const SummaryList = ({ summary, environment, responseCount, survey, local
} else {
// Add new filter
filterObject.filter.push({
questionType: value,
elementType: value,
filterType: {
filterComboBoxValue: filterComboBoxValue,
filterValue: filterValue,
},
});
toast.success(
constructToastMessage(questionType, filterValue, survey, questionId, t, filterComboBoxValue) ??
constructToastMessage(elementType, filterValue, survey, elementId, t, filterComboBoxValue) ??
t("environments.surveys.summary.filter_added_successfully"),
{ duration: 5000 }
);
@@ -111,12 +111,12 @@ export const SummaryList = ({ summary, environment, responseCount, survey, local
emptyMessage={t("environments.surveys.summary.no_responses_found")}
/>
) : (
summary.map((questionSummary) => {
if (questionSummary.type === TSurveyElementTypeEnum.OpenText) {
summary.map((elementSummary) => {
if (elementSummary.type === TSurveyElementTypeEnum.OpenText) {
return (
<OpenTextSummary
key={questionSummary.question.id}
questionSummary={questionSummary}
key={elementSummary.element.id}
elementSummary={elementSummary}
environmentId={environment.id}
survey={survey}
locale={locale}
@@ -124,13 +124,13 @@ export const SummaryList = ({ summary, environment, responseCount, survey, local
);
}
if (
questionSummary.type === TSurveyElementTypeEnum.MultipleChoiceSingle ||
questionSummary.type === TSurveyElementTypeEnum.MultipleChoiceMulti
elementSummary.type === TSurveyElementTypeEnum.MultipleChoiceSingle ||
elementSummary.type === TSurveyElementTypeEnum.MultipleChoiceMulti
) {
return (
<MultipleChoiceSummary
key={questionSummary.question.id}
questionSummary={questionSummary}
key={elementSummary.element.id}
elementSummary={elementSummary}
environmentId={environment.id}
surveyType={survey.type}
survey={survey}
@@ -138,132 +138,128 @@ export const SummaryList = ({ summary, environment, responseCount, survey, local
/>
);
}
if (questionSummary.type === TSurveyElementTypeEnum.NPS) {
if (elementSummary.type === TSurveyElementTypeEnum.NPS) {
return (
<NPSSummary
key={questionSummary.question.id}
questionSummary={questionSummary}
key={elementSummary.element.id}
elementSummary={elementSummary}
survey={survey}
setFilter={setFilter}
/>
);
}
if (questionSummary.type === TSurveyElementTypeEnum.CTA) {
if (elementSummary.type === TSurveyElementTypeEnum.CTA) {
return (
<CTASummary
key={questionSummary.question.id}
questionSummary={questionSummary}
survey={survey}
/>
<CTASummary key={elementSummary.element.id} elementSummary={elementSummary} survey={survey} />
);
}
if (questionSummary.type === TSurveyElementTypeEnum.Rating) {
if (elementSummary.type === TSurveyElementTypeEnum.Rating) {
return (
<RatingSummary
key={questionSummary.question.id}
questionSummary={questionSummary}
key={elementSummary.element.id}
elementSummary={elementSummary}
survey={survey}
setFilter={setFilter}
/>
);
}
if (questionSummary.type === TSurveyElementTypeEnum.Consent) {
if (elementSummary.type === TSurveyElementTypeEnum.Consent) {
return (
<ConsentSummary
key={questionSummary.question.id}
questionSummary={questionSummary}
key={elementSummary.element.id}
elementSummary={elementSummary}
survey={survey}
setFilter={setFilter}
/>
);
}
if (questionSummary.type === TSurveyElementTypeEnum.PictureSelection) {
if (elementSummary.type === TSurveyElementTypeEnum.PictureSelection) {
return (
<PictureChoiceSummary
key={questionSummary.question.id}
questionSummary={questionSummary}
key={elementSummary.element.id}
elementSummary={elementSummary}
survey={survey}
setFilter={setFilter}
/>
);
}
if (questionSummary.type === TSurveyElementTypeEnum.Date) {
if (elementSummary.type === TSurveyElementTypeEnum.Date) {
return (
<DateQuestionSummary
key={questionSummary.question.id}
questionSummary={questionSummary}
<DateElementSummary
key={elementSummary.element.id}
elementSummary={elementSummary}
environmentId={environment.id}
survey={survey}
locale={locale}
/>
);
}
if (questionSummary.type === TSurveyElementTypeEnum.FileUpload) {
if (elementSummary.type === TSurveyElementTypeEnum.FileUpload) {
return (
<FileUploadSummary
key={questionSummary.question.id}
questionSummary={questionSummary}
key={elementSummary.element.id}
elementSummary={elementSummary}
environmentId={environment.id}
survey={survey}
locale={locale}
/>
);
}
if (questionSummary.type === TSurveyElementTypeEnum.Cal) {
if (elementSummary.type === TSurveyElementTypeEnum.Cal) {
return (
<CalSummary
key={questionSummary.question.id}
questionSummary={questionSummary}
key={elementSummary.element.id}
elementSummary={elementSummary}
environmentId={environment.id}
survey={survey}
/>
);
}
if (questionSummary.type === TSurveyElementTypeEnum.Matrix) {
if (elementSummary.type === TSurveyElementTypeEnum.Matrix) {
return (
<MatrixQuestionSummary
key={questionSummary.question.id}
questionSummary={questionSummary}
<MatrixElementSummary
key={elementSummary.element.id}
elementSummary={elementSummary}
survey={survey}
setFilter={setFilter}
/>
);
}
if (questionSummary.type === TSurveyElementTypeEnum.Address) {
if (elementSummary.type === TSurveyElementTypeEnum.Address) {
return (
<AddressSummary
key={questionSummary.question.id}
questionSummary={questionSummary}
key={elementSummary.element.id}
elementSummary={elementSummary}
environmentId={environment.id}
survey={survey}
locale={locale}
/>
);
}
if (questionSummary.type === TSurveyElementTypeEnum.Ranking) {
if (elementSummary.type === TSurveyElementTypeEnum.Ranking) {
return (
<RankingSummary
key={questionSummary.question.id}
questionSummary={questionSummary}
key={elementSummary.element.id}
elementSummary={elementSummary}
survey={survey}
/>
);
}
if (questionSummary.type === "hiddenField") {
if (elementSummary.type === "hiddenField") {
return (
<HiddenFieldsSummary
key={questionSummary.id}
questionSummary={questionSummary}
key={elementSummary.id}
elementSummary={elementSummary}
environment={environment}
locale={locale}
/>
);
}
if (questionSummary.type === TSurveyElementTypeEnum.ContactInfo) {
if (elementSummary.type === TSurveyElementTypeEnum.ContactInfo) {
return (
<ContactInfoSummary
key={questionSummary.question.id}
questionSummary={questionSummary}
key={elementSummary.element.id}
elementSummary={elementSummary}
environmentId={environment.id}
survey={survey}
locale={locale}

View File

@@ -14,7 +14,7 @@ import { getSurvey } from "@/lib/survey/service";
import { evaluateLogic, performActions } from "@/lib/surveyLogic/utils";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import {
getQuestionSummary,
getElementSummary,
getResponsesForSummary,
getSurveySummary,
getSurveySummaryDropOff,
@@ -232,7 +232,7 @@ describe("getSurveySummaryDropOff", () => {
vi.mocked(evaluateLogic).mockReturnValue(false); // Default: no logic triggers
vi.mocked(performActions).mockReturnValue({
jumpTarget: undefined,
requiredQuestionIds: [],
requiredElementIds: [],
calculations: {},
});
});
@@ -270,14 +270,14 @@ describe("getSurveySummaryDropOff", () => {
expect(dropOff.length).toBe(2);
// Q1
expect(dropOff[0].questionId).toBe("q1");
expect(dropOff[0].elementId).toBe("q1");
expect(dropOff[0].impressions).toBe(displayCount); // Welcome card disabled, so first question impressions = displayCount
expect(dropOff[0].dropOffCount).toBe(displayCount - responses.length); // 5 displays - 2 started = 3 dropped before q1
expect(dropOff[0].dropOffPercentage).toBe(60); // (3/5)*100
expect(dropOff[0].ttc).toBe(10);
// Q2
expect(dropOff[1].questionId).toBe("q2");
expect(dropOff[1].elementId).toBe("q2");
expect(dropOff[1].impressions).toBe(responses.length); // 2 responses reached q1, so 2 impressions for q2
expect(dropOff[1].dropOffCount).toBe(1); // 1 response dropped at q2
expect(dropOff[1].dropOffPercentage).toBe(50); // (1/2)*100
@@ -395,9 +395,9 @@ describe("getSurveySummaryDropOff", () => {
});
vi.mocked(performActions).mockImplementation((_s, actions, _d, _v) => {
if (actions[0] && "objective" in actions[0] && actions[0].objective === "jumpToBlock") {
return { jumpTarget: actions[0].target, requiredQuestionIds: [], calculations: {} };
return { jumpTarget: actions[0].target, requiredElementIds: [], calculations: {} };
}
return { jumpTarget: undefined, requiredQuestionIds: [], calculations: {} };
return { jumpTarget: undefined, requiredElementIds: [], calculations: {} };
});
const dropOff = getSurveySummaryDropOff(
@@ -471,7 +471,7 @@ describe("getQuestionSummary", () => {
});
test("summarizes OpenText questions", async () => {
const summary = await getQuestionSummary(
const summary = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -485,7 +485,7 @@ describe("getQuestionSummary", () => {
});
test("summarizes MultipleChoiceSingle questions", async () => {
const summary = await getQuestionSummary(
const summary = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -503,7 +503,7 @@ describe("getQuestionSummary", () => {
});
test("summarizes HiddenFields", async () => {
const summary = await getQuestionSummary(
const summary = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -568,10 +568,10 @@ describe("getQuestionSummary", () => {
];
const dropOff = [
{ questionId: "ranking-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "ranking-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary = await getQuestionSummary(
const summary = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -654,10 +654,10 @@ describe("getQuestionSummary", () => {
});
const dropOff = [
{ questionId: "ranking-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "ranking-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary = await getQuestionSummary(
const summary = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -736,10 +736,10 @@ describe("getQuestionSummary", () => {
];
const dropOff = [
{ questionId: "ranking-q1", impressions: 2, dropOffCount: 2, dropOffPercentage: 100 },
{ elementId: "ranking-q1", impressions: 2, dropOffCount: 2, dropOffPercentage: 100 },
] as unknown as TSurveySummary["dropOff"];
const summary = await getQuestionSummary(
const summary = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -800,10 +800,10 @@ describe("getQuestionSummary", () => {
];
const dropOff = [
{ questionId: "ranking-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "ranking-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary = await getQuestionSummary(
const summary = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -864,10 +864,10 @@ describe("getQuestionSummary", () => {
];
const dropOff = [
{ questionId: "ranking-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "ranking-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary = await getQuestionSummary(
const summary = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -1242,15 +1242,10 @@ describe("Address and ContactInfo question types", () => {
];
const dropOff = [
{ questionId: "address-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "address-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary = await getQuestionSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
dropOff
);
const summary = await getElementSummary(survey, getElementsFromBlocks(survey.blocks), responses, dropOff);
expect(summary).toHaveLength(1);
expect(summary[0].type).toBe(TSurveyElementTypeEnum.Address);
@@ -1319,15 +1314,10 @@ describe("Address and ContactInfo question types", () => {
] as any;
const dropOff = [
{ questionId: "contact-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "contact-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary = await getQuestionSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
dropOff
);
const summary = await getElementSummary(survey, getElementsFromBlocks(survey.blocks), responses, dropOff);
expect(summary).toHaveLength(1);
expect(summary[0].type).toBe(TSurveyElementTypeEnum.ContactInfo);
@@ -1374,15 +1364,10 @@ describe("Address and ContactInfo question types", () => {
];
const dropOff = [
{ questionId: "address-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "address-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary = await getQuestionSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
dropOff
);
const summary = await getElementSummary(survey, getElementsFromBlocks(survey.blocks), responses, dropOff);
expect(summary).toHaveLength(1);
expect((summary[0] as any).type).toBe(TSurveyElementTypeEnum.Address);
@@ -1447,15 +1432,10 @@ describe("Address and ContactInfo question types", () => {
] as any;
const dropOff = [
{ questionId: "contact-q1", impressions: 3, dropOffCount: 3, dropOffPercentage: 100 },
{ elementId: "contact-q1", impressions: 3, dropOffCount: 3, dropOffPercentage: 100 },
] as unknown as TSurveySummary["dropOff"];
const summary = await getQuestionSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
dropOff
);
const summary = await getElementSummary(survey, getElementsFromBlocks(survey.blocks), responses, dropOff);
expect(summary).toHaveLength(1);
expect((summary[0] as any).type).toBe(TSurveyElementTypeEnum.ContactInfo);
@@ -1516,15 +1496,10 @@ describe("Address and ContactInfo question types", () => {
] as any;
const dropOff = [
{ questionId: "address-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "address-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary = await getQuestionSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
dropOff
);
const summary = await getElementSummary(survey, getElementsFromBlocks(survey.blocks), responses, dropOff);
expect(summary).toHaveLength(1);
expect((summary[0] as any).type).toBe(TSurveyElementTypeEnum.Address);
@@ -1579,15 +1554,10 @@ describe("Address and ContactInfo question types", () => {
);
const dropOff = [
{ questionId: "contact-q1", impressions: 100, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "contact-q1", impressions: 100, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary = await getQuestionSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
dropOff
);
const summary = await getElementSummary(survey, getElementsFromBlocks(survey.blocks), responses, dropOff);
expect(summary).toHaveLength(1);
expect((summary[0] as any).type).toBe(TSurveyElementTypeEnum.ContactInfo);
@@ -1666,10 +1636,10 @@ describe("Matrix question type tests", () => {
];
const dropOff = [
{ questionId: "matrix-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "matrix-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -1754,7 +1724,7 @@ describe("Matrix question type tests", () => {
];
const dropOff = [
{ questionId: "matrix-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "matrix-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
// Mock getLocalizedValue for this test
@@ -1773,7 +1743,7 @@ describe("Matrix question type tests", () => {
return "";
});
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -1879,10 +1849,10 @@ describe("Matrix question type tests", () => {
] as any;
const dropOff = [
{ questionId: "matrix-q1", impressions: 4, dropOffCount: 4, dropOffPercentage: 100 },
{ elementId: "matrix-q1", impressions: 4, dropOffCount: 4, dropOffPercentage: 100 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -1971,10 +1941,10 @@ describe("Matrix question type tests", () => {
] as any;
const dropOff = [
{ questionId: "matrix-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "matrix-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -2043,10 +2013,10 @@ describe("Matrix question type tests", () => {
] as any;
const dropOff = [
{ questionId: "matrix-q1", impressions: 1, dropOffCount: 1, dropOffPercentage: 100 },
{ elementId: "matrix-q1", impressions: 1, dropOffCount: 1, dropOffPercentage: 100 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -2120,10 +2090,10 @@ describe("Matrix question type tests", () => {
];
const dropOff = [
{ questionId: "matrix-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "matrix-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -2202,10 +2172,10 @@ describe("Matrix question type tests", () => {
];
const dropOff = [
{ questionId: "matrix-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "matrix-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -2298,7 +2268,7 @@ describe("Matrix question type tests", () => {
] as any;
const dropOff = [
{ questionId: "matrix-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "matrix-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
// Mock getLocalizedValue to handle our specific test case
@@ -2317,7 +2287,7 @@ describe("Matrix question type tests", () => {
return "";
});
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -2381,10 +2351,10 @@ describe("Matrix question type tests", () => {
] as any;
const dropOff = [
{ questionId: "matrix-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "matrix-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -2474,10 +2444,10 @@ describe("NPS question type tests", () => {
];
const dropOff = [
{ questionId: "nps-q1", impressions: 4, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "nps-q1", impressions: 4, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -2568,10 +2538,10 @@ describe("NPS question type tests", () => {
] as any;
const dropOff = [
{ questionId: "nps-q1", impressions: 3, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "nps-q1", impressions: 3, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -2635,10 +2605,10 @@ describe("NPS question type tests", () => {
];
const dropOff = [
{ questionId: "nps-q1", impressions: 1, dropOffCount: 1, dropOffPercentage: 100 },
{ elementId: "nps-q1", impressions: 1, dropOffCount: 1, dropOffPercentage: 100 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -2721,10 +2691,10 @@ describe("NPS question type tests", () => {
] as any;
const dropOff = [
{ questionId: "nps-q1", impressions: 3, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "nps-q1", impressions: 3, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -2815,10 +2785,10 @@ describe("Rating question type tests", () => {
];
const dropOff = [
{ questionId: "rating-q1", impressions: 4, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "rating-q1", impressions: 4, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -2917,10 +2887,10 @@ describe("Rating question type tests", () => {
] as any;
const dropOff = [
{ questionId: "rating-q1", impressions: 3, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "rating-q1", impressions: 3, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -2977,10 +2947,10 @@ describe("Rating question type tests", () => {
];
const dropOff = [
{ questionId: "rating-q1", impressions: 1, dropOffCount: 1, dropOffPercentage: 100 },
{ elementId: "rating-q1", impressions: 1, dropOffCount: 1, dropOffPercentage: 100 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -3055,10 +3025,10 @@ describe("PictureSelection question type tests", () => {
];
const dropOff = [
{ questionId: "picture-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "picture-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -3134,10 +3104,10 @@ describe("PictureSelection question type tests", () => {
] as any;
const dropOff = [
{ questionId: "picture-q1", impressions: 2, dropOffCount: 2, dropOffPercentage: 100 },
{ elementId: "picture-q1", impressions: 2, dropOffCount: 2, dropOffPercentage: 100 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -3196,10 +3166,10 @@ describe("PictureSelection question type tests", () => {
];
const dropOff = [
{ questionId: "picture-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "picture-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -3287,14 +3257,14 @@ describe("CTA question type tests", () => {
const dropOff = [
{
questionId: "cta-q1",
elementId: "cta-q1",
impressions: 5, // 5 total impressions (including 2 that didn't respond)
dropOffCount: 0,
dropOffPercentage: 0,
},
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -3353,14 +3323,14 @@ describe("CTA question type tests", () => {
const dropOff = [
{
questionId: "cta-q1",
elementId: "cta-q1",
impressions: 3, // 3 total impressions
dropOffCount: 3,
dropOffPercentage: 100,
},
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -3437,10 +3407,10 @@ describe("Consent question type tests", () => {
] as any;
const dropOff = [
{ questionId: "consent-q1", impressions: 3, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "consent-q1", impressions: 3, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -3497,10 +3467,10 @@ describe("Consent question type tests", () => {
];
const dropOff = [
{ questionId: "consent-q1", impressions: 1, dropOffCount: 1, dropOffPercentage: 100 },
{ elementId: "consent-q1", impressions: 1, dropOffCount: 1, dropOffPercentage: 100 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -3553,10 +3523,10 @@ describe("Consent question type tests", () => {
];
const dropOff = [
{ questionId: "consent-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "consent-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -3618,10 +3588,10 @@ describe("Date question type tests", () => {
];
const dropOff = [
{ questionId: "date-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "date-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -3677,10 +3647,10 @@ describe("Date question type tests", () => {
];
const dropOff = [
{ questionId: "date-q1", impressions: 1, dropOffCount: 1, dropOffPercentage: 100 },
{ elementId: "date-q1", impressions: 1, dropOffCount: 1, dropOffPercentage: 100 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -3728,10 +3698,10 @@ describe("Date question type tests", () => {
}));
const dropOff = [
{ questionId: "date-q1", impressions: 100, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "date-q1", impressions: 100, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -3796,10 +3766,10 @@ describe("FileUpload question type tests", () => {
];
const dropOff = [
{ questionId: "file-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "file-q1", impressions: 2, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -3855,10 +3825,10 @@ describe("FileUpload question type tests", () => {
];
const dropOff = [
{ questionId: "file-q1", impressions: 1, dropOffCount: 1, dropOffPercentage: 100 },
{ elementId: "file-q1", impressions: 1, dropOffCount: 1, dropOffPercentage: 100 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -3931,10 +3901,10 @@ describe("Cal question type tests", () => {
] as any;
const dropOff = [
{ questionId: "cal-q1", impressions: 3, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "cal-q1", impressions: 3, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -3992,10 +3962,10 @@ describe("Cal question type tests", () => {
];
const dropOff = [
{ questionId: "cal-q1", impressions: 1, dropOffCount: 1, dropOffPercentage: 100 },
{ elementId: "cal-q1", impressions: 1, dropOffCount: 1, dropOffPercentage: 100 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,
@@ -4049,10 +4019,10 @@ describe("Cal question type tests", () => {
];
const dropOff = [
{ questionId: "cal-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
{ elementId: "cal-q1", impressions: 1, dropOffCount: 0, dropOffPercentage: 0 },
] as unknown as TSurveySummary["dropOff"];
const summary: any = await getQuestionSummary(
const summary: any = await getElementSummary(
survey,
getElementsFromBlocks(survey.blocks),
responses,

View File

@@ -15,8 +15,6 @@ import {
ZResponseFilterCriteria,
} from "@formbricks/types/responses";
import {
TSurveyAddressElement,
TSurveyContactInfoElement,
TSurveyElement,
TSurveyElementChoice,
TSurveyElementTypeEnum,
@@ -99,16 +97,16 @@ export const getSurveySummaryMeta = (
};
};
const evaluateLogicAndGetNextQuestionId = (
const evaluateLogicAndGetNextElementId = (
localSurvey: TSurvey,
questions: TSurveyElement[],
elements: TSurveyElement[],
data: TResponseData,
localVariables: TResponseVariables,
currentQuestionIndex: number,
currentElementIndex: number,
currQuesTemp: TSurveyElement,
selectedLanguage: string | null
): {
nextQuestionId: string | undefined;
nextElementId: string | undefined;
updatedSurvey: TSurvey;
updatedVariables: TResponseVariables;
} => {
@@ -122,19 +120,19 @@ const evaluateLogicAndGetNextQuestionId = (
if (currentBlock?.logic && currentBlock.logic.length > 0) {
for (const logic of currentBlock.logic) {
if (evaluateLogic(localSurvey, data, localVariables, logic.conditions, selectedLanguage ?? "default")) {
const { jumpTarget, requiredQuestionIds, calculations } = performActions(
const { jumpTarget, requiredElementIds, calculations } = performActions(
updatedSurvey,
logic.actions,
data,
updatedVariables
);
if (requiredQuestionIds.length > 0) {
if (requiredElementIds.length > 0) {
// Update blocks to mark elements as required
updatedSurvey.blocks = updatedSurvey.blocks.map((block) => ({
...block,
elements: block.elements.map((e) =>
requiredQuestionIds.includes(e.id) ? { ...e, required: true } : e
requiredElementIds.includes(e.id) ? { ...e, required: true } : e
),
}));
}
@@ -152,29 +150,29 @@ const evaluateLogicAndGetNextQuestionId = (
firstJumpTarget = currentBlock.logicFallback;
}
// Return the first jump target if found, otherwise go to the next question
const nextQuestionId = firstJumpTarget || questions[currentQuestionIndex + 1]?.id || undefined;
// Return the first jump target if found, otherwise go to the next element
const nextElementId = firstJumpTarget || elements[currentElementIndex + 1]?.id || undefined;
return { nextQuestionId, updatedSurvey, updatedVariables };
return { nextElementId, updatedSurvey, updatedVariables };
};
export const getSurveySummaryDropOff = (
survey: TSurvey,
questions: TSurveyElement[],
elements: TSurveyElement[],
responses: TSurveySummaryResponse[],
displayCount: number
): TSurveySummary["dropOff"] => {
const initialTtc = questions.reduce((acc: Record<string, number>, question) => {
acc[question.id] = 0;
const initialTtc = elements.reduce((acc: Record<string, number>, element) => {
acc[element.id] = 0;
return acc;
}, {});
let totalTtc = { ...initialTtc };
let responseCounts = { ...initialTtc };
let dropOffArr = new Array(questions.length).fill(0) as number[];
let impressionsArr = new Array(questions.length).fill(0) as number[];
let dropOffPercentageArr = new Array(questions.length).fill(0) as number[];
let dropOffArr = new Array(elements.length).fill(0) as number[];
let impressionsArr = new Array(elements.length).fill(0) as number[];
let dropOffPercentageArr = new Array(elements.length).fill(0) as number[];
const surveyVariablesData = survey.variables?.reduce(
(acc, variable) => {
@@ -186,10 +184,10 @@ export const getSurveySummaryDropOff = (
responses.forEach((response) => {
// Calculate total time-to-completion
Object.keys(totalTtc).forEach((questionId) => {
if (response.ttc && response.ttc[questionId]) {
totalTtc[questionId] += response.ttc[questionId];
responseCounts[questionId]++;
Object.keys(totalTtc).forEach((elementId) => {
if (response.ttc && response.ttc[elementId]) {
totalTtc[elementId] += response.ttc[elementId];
responseCounts[elementId]++;
}
});
@@ -201,11 +199,11 @@ export const getSurveySummaryDropOff = (
let currQuesIdx = 0;
while (currQuesIdx < questions.length) {
const currQues = questions[currQuesIdx];
while (currQuesIdx < elements.length) {
const currQues = elements[currQuesIdx];
if (!currQues) break;
// question is not answered and required
// element is not answered and required
if (response.data[currQues.id] === undefined && currQues.required) {
dropOffArr[currQuesIdx]++;
impressionsArr[currQuesIdx]++;
@@ -214,9 +212,9 @@ export const getSurveySummaryDropOff = (
impressionsArr[currQuesIdx]++;
const { nextQuestionId, updatedSurvey, updatedVariables } = evaluateLogicAndGetNextQuestionId(
const { nextElementId, updatedSurvey, updatedVariables } = evaluateLogicAndGetNextElementId(
localSurvey,
questions,
elements,
localResponseData,
localVariables,
currQuesIdx,
@@ -227,9 +225,9 @@ export const getSurveySummaryDropOff = (
localSurvey = updatedSurvey;
localVariables = updatedVariables;
if (nextQuestionId) {
const nextQuesIdx = questions.findIndex((q) => q.id === nextQuestionId);
if (!response.data[nextQuestionId] && !response.finished) {
if (nextElementId) {
const nextQuesIdx = elements.findIndex((q) => q.id === nextElementId);
if (!response.data[nextElementId] && !response.finished) {
dropOffArr[nextQuesIdx]++;
impressionsArr[nextQuesIdx]++;
break;
@@ -241,10 +239,9 @@ export const getSurveySummaryDropOff = (
}
});
// Calculate the average time for each question
Object.keys(totalTtc).forEach((questionId) => {
totalTtc[questionId] =
responseCounts[questionId] > 0 ? totalTtc[questionId] / responseCounts[questionId] : 0;
// Calculate the average time for each element
Object.keys(totalTtc).forEach((elementId) => {
totalTtc[elementId] = responseCounts[elementId] > 0 ? totalTtc[elementId] / responseCounts[elementId] : 0;
});
if (!survey.welcomeCard.enabled) {
@@ -261,18 +258,18 @@ export const getSurveySummaryDropOff = (
dropOffPercentageArr[0] = (dropOffArr[0] / impressionsArr[0]) * 100;
}
for (let i = 1; i < questions.length; i++) {
for (let i = 1; i < elements.length; i++) {
if (impressionsArr[i] !== 0) {
dropOffPercentageArr[i] = (dropOffArr[i] / impressionsArr[i]) * 100;
}
}
const dropOff = questions.map((question, index) => {
const dropOff = elements.map((element, index) => {
return {
questionId: question.id,
questionType: question.type,
headline: getTextContent(getLocalizedValue(question.headline, "default")),
ttc: convertFloatTo2Decimal(totalTtc[question.id]) || 0,
elementId: element.id,
elementType: element.type,
headline: getTextContent(getLocalizedValue(element.headline, "default")),
ttc: convertFloatTo2Decimal(totalTtc[element.id]) || 0,
impressions: impressionsArr[index] || 0,
dropOffCount: dropOffArr[index] || 0,
dropOffPercentage: convertFloatTo2Decimal(dropOffPercentageArr[index]) || 0,
@@ -291,17 +288,17 @@ const getLanguageCode = (surveyLanguages: TSurveyLanguage[], languageCode: strin
const checkForI18n = (
responseData: TResponseData,
id: string,
questions: TSurveyElement[],
elements: TSurveyElement[],
languageCode: string
) => {
const question = questions.find((question) => question.id === id);
const element = elements.find((element) => element.id === id);
if (question?.type === "multipleChoiceMulti" || question?.type === "ranking") {
if (element?.type === "multipleChoiceMulti" || element?.type === "ranking") {
// Initialize an array to hold the choice values
let choiceValues = [] as string[];
// Type guard: both question types have choices property
const hasChoices = "choices" in question;
// Type guard: both element types have choices property
const hasChoices = "choices" in element;
if (!hasChoices) return [];
(typeof responseData[id] === "string"
@@ -310,19 +307,19 @@ const checkForI18n = (
)?.forEach((data) => {
choiceValues.push(
getLocalizedValue(
question.choices.find((choice) => choice.label[languageCode] === data)?.label,
element.choices.find((choice) => choice.label[languageCode] === data)?.label,
"default"
) || data
);
});
// Return the array of localized choice values of multiSelect multi questions
// Return the array of localized choice values of multiSelect multi elements
return choiceValues;
}
// Return the localized value of the choice fo multiSelect single question
if (question && "choices" in question) {
const choice = question.choices?.find(
// Return the localized value of the choice fo multiSelect single element
if (element && "choices" in element) {
const choice = element.choices?.find(
(choice: TSurveyElementChoice) => choice.label?.[languageCode] === responseData[id]
);
return choice && "label" in choice
@@ -333,21 +330,21 @@ const checkForI18n = (
return responseData[id];
};
export const getQuestionSummary = async (
export const getElementSummary = async (
survey: TSurvey,
questions: TSurveyElement[],
elements: TSurveyElement[],
responses: TSurveySummaryResponse[],
dropOff: TSurveySummary["dropOff"]
): Promise<TSurveySummary["summary"]> => {
const VALUES_LIMIT = 50;
let summary: TSurveySummary["summary"] = [];
for (const question of questions) {
switch (question.type) {
for (const element of elements) {
switch (element.type) {
case TSurveyElementTypeEnum.OpenText: {
let values: TSurveyElementSummaryOpenText["samples"] = [];
responses.forEach((response) => {
const answer = response.data[question.id];
const answer = response.data[element.id];
if (answer && typeof answer === "string") {
values.push({
id: response.id,
@@ -360,8 +357,8 @@ export const getQuestionSummary = async (
});
summary.push({
type: question.type,
question: question,
type: element.type,
element: element,
responseCount: values.length,
samples: values.slice(0, VALUES_LIMIT),
});
@@ -373,14 +370,14 @@ export const getQuestionSummary = async (
case TSurveyElementTypeEnum.MultipleChoiceMulti: {
let values: TSurveyElementSummaryMultipleChoice["choices"] = [];
const otherOption = question.choices.find((choice) => choice.id === "other");
const noneOption = question.choices.find((choice) => choice.id === "none");
const otherOption = element.choices.find((choice) => choice.id === "other");
const noneOption = element.choices.find((choice) => choice.id === "none");
const questionChoices = question.choices
const elementChoices = element.choices
.filter((choice) => choice.id !== "other" && choice.id !== "none")
.map((choice) => getLocalizedValue(choice.label, "default"));
const choiceCountMap = questionChoices.reduce((acc: Record<string, number>, choice) => {
const choiceCountMap = elementChoices.reduce((acc: Record<string, number>, choice) => {
acc[choice] = 0;
return acc;
}, {});
@@ -397,16 +394,16 @@ export const getQuestionSummary = async (
const answer =
responseLanguageCode === "default"
? response.data[question.id]
: checkForI18n(response.data, question.id, questions, responseLanguageCode);
? response.data[element.id]
: checkForI18n(response.data, element.id, elements, responseLanguageCode);
let hasValidAnswer = false;
if (Array.isArray(answer) && question.type === TSurveyElementTypeEnum.MultipleChoiceMulti) {
if (Array.isArray(answer) && element.type === TSurveyElementTypeEnum.MultipleChoiceMulti) {
answer.forEach((value) => {
if (value) {
totalSelectionCount++;
if (questionChoices.includes(value)) {
if (elementChoices.includes(value)) {
choiceCountMap[value]++;
} else if (noneLabel && value === noneLabel) {
noneCount++;
@@ -422,11 +419,11 @@ export const getQuestionSummary = async (
});
} else if (
typeof answer === "string" &&
question.type === TSurveyElementTypeEnum.MultipleChoiceSingle
element.type === TSurveyElementTypeEnum.MultipleChoiceSingle
) {
if (answer) {
totalSelectionCount++;
if (questionChoices.includes(answer)) {
if (elementChoices.includes(answer)) {
choiceCountMap[answer]++;
} else if (noneLabel && answer === noneLabel) {
noneCount++;
@@ -478,8 +475,8 @@ export const getQuestionSummary = async (
}
summary.push({
type: question.type,
question,
type: element.type,
element,
responseCount: totalResponseCount,
selectionCount: totalSelectionCount,
choices: values,
@@ -492,14 +489,14 @@ export const getQuestionSummary = async (
let values: TSurveyElementSummaryPictureSelection["choices"] = [];
const choiceCountMap: Record<string, number> = {};
question.choices.forEach((choice) => {
element.choices.forEach((choice) => {
choiceCountMap[choice.id] = 0;
});
let totalResponseCount = 0;
let totalSelectionCount = 0;
responses.forEach((response) => {
const answer = response.data[question.id];
const answer = response.data[element.id];
if (Array.isArray(answer)) {
totalResponseCount++;
answer.forEach((value) => {
@@ -509,7 +506,7 @@ export const getQuestionSummary = async (
}
});
question.choices.forEach((choice) => {
element.choices.forEach((choice) => {
values.push({
id: choice.id,
imageUrl: choice.imageUrl,
@@ -522,8 +519,8 @@ export const getQuestionSummary = async (
});
summary.push({
type: question.type,
question,
type: element.type,
element,
responseCount: totalResponseCount,
selectionCount: totalSelectionCount,
choices: values,
@@ -535,7 +532,7 @@ export const getQuestionSummary = async (
case TSurveyElementTypeEnum.Rating: {
let values: TSurveyElementSummaryRating["choices"] = [];
const choiceCountMap: Record<number, number> = {};
const range = question.range;
const range = element.range;
for (let i = 1; i <= range; i++) {
choiceCountMap[i] = 0;
@@ -546,12 +543,12 @@ export const getQuestionSummary = async (
let dismissed = 0;
responses.forEach((response) => {
const answer = response.data[question.id];
const answer = response.data[element.id];
if (typeof answer === "number") {
totalResponseCount++;
choiceCountMap[answer]++;
totalRating += answer;
} else if (response.ttc && response.ttc[question.id] > 0) {
} else if (response.ttc && response.ttc[element.id] > 0) {
dismissed++;
}
});
@@ -566,8 +563,8 @@ export const getQuestionSummary = async (
});
summary.push({
type: question.type,
question,
type: element.type,
element,
average: convertFloatTo2Decimal(totalRating / totalResponseCount) || 0,
responseCount: totalResponseCount,
choices: values,
@@ -590,7 +587,7 @@ export const getQuestionSummary = async (
};
responses.forEach((response) => {
const value = response.data[question.id];
const value = response.data[element.id];
if (typeof value === "number") {
data.total++;
if (value >= 9) {
@@ -600,7 +597,7 @@ export const getQuestionSummary = async (
} else {
data.detractors++;
}
} else if (response.ttc && response.ttc[question.id] > 0) {
} else if (response.ttc && response.ttc[element.id] > 0) {
data.total++;
data.dismissed++;
}
@@ -612,8 +609,8 @@ export const getQuestionSummary = async (
: 0;
summary.push({
type: question.type,
question,
type: element.type,
element,
responseCount: data.total,
total: data.total,
score: data.score,
@@ -643,7 +640,7 @@ export const getQuestionSummary = async (
};
responses.forEach((response) => {
const value = response.data[question.id];
const value = response.data[element.id];
if (value === "clicked") {
data.clicked++;
} else if (value === "dismissed") {
@@ -652,12 +649,12 @@ export const getQuestionSummary = async (
});
const totalResponses = data.clicked + data.dismissed;
const idx = questions.findIndex((q) => q.id === question.id);
const idx = elements.findIndex((q) => q.id === element.id);
const impressions = dropOff[idx].impressions;
summary.push({
type: question.type,
question,
type: element.type,
element,
impressionCount: impressions,
clickCount: data.clicked,
skipCount: data.dismissed,
@@ -676,10 +673,10 @@ export const getQuestionSummary = async (
};
responses.forEach((response) => {
const value = response.data[question.id];
const value = response.data[element.id];
if (value === "accepted") {
data.accepted++;
} else if (response.ttc && response.ttc[question.id] > 0) {
} else if (response.ttc && response.ttc[element.id] > 0) {
data.dismissed++;
}
});
@@ -687,8 +684,8 @@ export const getQuestionSummary = async (
const totalResponses = data.accepted + data.dismissed;
summary.push({
type: question.type,
question,
type: element.type,
element,
responseCount: totalResponses,
accepted: {
count: data.accepted,
@@ -707,7 +704,7 @@ export const getQuestionSummary = async (
case TSurveyElementTypeEnum.Date: {
let values: TSurveyElementSummaryDate["samples"] = [];
responses.forEach((response) => {
const answer = response.data[question.id];
const answer = response.data[element.id];
if (answer && typeof answer === "string") {
values.push({
id: response.id,
@@ -720,8 +717,8 @@ export const getQuestionSummary = async (
});
summary.push({
type: question.type,
question,
type: element.type,
element,
responseCount: values.length,
samples: values.slice(0, VALUES_LIMIT),
});
@@ -732,7 +729,7 @@ export const getQuestionSummary = async (
case TSurveyElementTypeEnum.FileUpload: {
let values: TSurveyElementSummaryFileUpload["files"] = [];
responses.forEach((response) => {
const answer = response.data[question.id];
const answer = response.data[element.id];
if (Array.isArray(answer)) {
values.push({
id: response.id,
@@ -745,8 +742,8 @@ export const getQuestionSummary = async (
});
summary.push({
type: question.type,
question,
type: element.type,
element,
responseCount: values.length,
files: values.slice(0, VALUES_LIMIT),
});
@@ -761,18 +758,18 @@ export const getQuestionSummary = async (
};
responses.forEach((response) => {
const value = response.data[question.id];
const value = response.data[element.id];
if (value === "booked") {
data.booked++;
} else if (response.ttc && response.ttc[question.id] > 0) {
} else if (response.ttc && response.ttc[element.id] > 0) {
data.skipped++;
}
});
const totalResponses = data.booked + data.skipped;
summary.push({
type: question.type,
question,
type: element.type,
element,
responseCount: totalResponses,
booked: {
count: data.booked,
@@ -788,8 +785,8 @@ export const getQuestionSummary = async (
break;
}
case TSurveyElementTypeEnum.Matrix: {
const rows = question.rows.map((row) => getLocalizedValue(row.label, "default"));
const columns = question.columns.map((column) => getLocalizedValue(column.label, "default"));
const rows = element.rows.map((row) => getLocalizedValue(row.label, "default"));
const columns = element.columns.map((column) => getLocalizedValue(column.label, "default"));
let totalResponseCount = 0;
// Initialize count object
@@ -802,13 +799,13 @@ export const getQuestionSummary = async (
}, {});
responses.forEach((response) => {
const selectedResponses = response.data[question.id] as Record<string, string>;
const selectedResponses = response.data[element.id] as Record<string, string>;
const responseLanguageCode = getLanguageCode(survey.languages, response.language);
if (selectedResponses) {
totalResponseCount++;
question.rows.forEach((row) => {
element.rows.forEach((row) => {
const localizedRow = getLocalizedValue(row.label, responseLanguageCode);
const colValue = question.columns.find((column) => {
const colValue = element.columns.find((column) => {
return (
getLocalizedValue(column.label, responseLanguageCode) === selectedResponses[localizedRow]
);
@@ -841,8 +838,8 @@ export const getQuestionSummary = async (
});
summary.push({
type: question.type,
question,
type: element.type,
element,
responseCount: totalResponseCount,
data: matrixSummary,
});
@@ -851,7 +848,7 @@ export const getQuestionSummary = async (
case TSurveyElementTypeEnum.Address: {
let values: TSurveyElementSummaryAddress["samples"] = [];
responses.forEach((response) => {
const answer = response.data[question.id];
const answer = response.data[element.id];
if (Array.isArray(answer) && answer.length > 0) {
values.push({
id: response.id,
@@ -865,7 +862,7 @@ export const getQuestionSummary = async (
summary.push({
type: TSurveyElementTypeEnum.Address,
question: question as TSurveyAddressElement,
element,
responseCount: values.length,
samples: values.slice(0, VALUES_LIMIT),
});
@@ -876,7 +873,7 @@ export const getQuestionSummary = async (
case TSurveyElementTypeEnum.ContactInfo: {
let values: TSurveyElementSummaryContactInfo["samples"] = [];
responses.forEach((response) => {
const answer = response.data[question.id];
const answer = response.data[element.id];
if (Array.isArray(answer) && answer.length > 0) {
values.push({
id: response.id,
@@ -890,7 +887,7 @@ export const getQuestionSummary = async (
summary.push({
type: TSurveyElementTypeEnum.ContactInfo,
question: question as TSurveyContactInfoElement,
element,
responseCount: values.length,
samples: values.slice(0, VALUES_LIMIT),
});
@@ -900,11 +897,12 @@ export const getQuestionSummary = async (
}
case TSurveyElementTypeEnum.Ranking: {
let values: TSurveyElementSummaryRanking["choices"] = [];
const questionChoices = question.choices.map((choice) => getLocalizedValue(choice.label, "default"));
const elementChoices = element.choices.map((choice) => getLocalizedValue(choice.label, "default"));
let totalResponseCount = 0;
const choiceRankSums: Record<string, number> = {};
const choiceCountMap: Record<string, number> = {};
questionChoices.forEach((choice: string) => {
elementChoices.forEach((choice: string) => {
choiceRankSums[choice] = 0;
choiceCountMap[choice] = 0;
});
@@ -914,14 +912,14 @@ export const getQuestionSummary = async (
const answer =
responseLanguageCode === "default"
? response.data[question.id]
: checkForI18n(response.data, question.id, questions, responseLanguageCode);
? response.data[element.id]
: checkForI18n(response.data, element.id, elements, responseLanguageCode);
if (Array.isArray(answer)) {
totalResponseCount++;
answer.forEach((value, index) => {
const ranking = index + 1; // Calculate ranking based on index
if (questionChoices.includes(value)) {
if (elementChoices.includes(value)) {
choiceRankSums[value] += ranking;
choiceCountMap[value]++;
}
@@ -929,7 +927,7 @@ export const getQuestionSummary = async (
}
});
questionChoices.forEach((choice: string) => {
elementChoices.forEach((choice: string) => {
const count = choiceCountMap[choice];
const avgRanking = count > 0 ? choiceRankSums[choice] / count : 0;
values.push({
@@ -940,8 +938,8 @@ export const getQuestionSummary = async (
});
summary.push({
type: question.type,
question,
type: element.type,
element,
responseCount: totalResponseCount,
choices: values,
});
@@ -988,8 +986,7 @@ export const getSurveySummary = reactCache(
throw new ResourceNotFoundError("Survey", surveyId);
}
// Derive questions once from blocks
const questions = getElementsFromBlocks(survey.blocks);
const elements = getElementsFromBlocks(survey.blocks);
const batchSize = 5000;
const hasFilter = Object.keys(filterCriteria ?? {}).length > 0;
@@ -1021,16 +1018,16 @@ export const getSurveySummary = reactCache(
getQuotasSummary(surveyId),
]);
const dropOff = getSurveySummaryDropOff(survey, questions, responses, displayCount);
const [meta, questionWiseSummary] = await Promise.all([
const dropOff = getSurveySummaryDropOff(survey, elements, responses, displayCount);
const [meta, elementSummary] = await Promise.all([
getSurveySummaryMeta(responses, displayCount, quotas),
getQuestionSummary(survey, questions, responses, dropOff),
getElementSummary(survey, elements, responses, dropOff),
]);
return {
meta,
dropOff,
summary: questionWiseSummary,
summary: elementSummary,
quotas,
};
} catch (error) {

View File

@@ -1,6 +1,6 @@
import { TFunction } from "i18next";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { TSurvey } from "@formbricks/types/surveys/types";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
export const convertFloatToNDecimal = (num: number, N: number = 2) => {
@@ -12,29 +12,28 @@ export const convertFloatTo2Decimal = (num: number) => {
};
export const constructToastMessage = (
questionType: TSurveyElementTypeEnum,
elementType: TSurveyElementTypeEnum,
filterValue: string,
survey: TSurvey,
questionId: TSurveyQuestionId,
elementId: string,
t: TFunction,
filterComboBoxValue?: string | string[]
) => {
// Derive questions from blocks
const questions = getElementsFromBlocks(survey.blocks);
const questionIdx = questions.findIndex((question) => question.id === questionId);
if (questionType === "matrix") {
const elements = getElementsFromBlocks(survey.blocks);
const elementIdx = elements.findIndex((element) => element.id === elementId);
if (elementType === "matrix") {
return t("environments.surveys.summary.added_filter_for_responses_where_answer_to_question", {
questionIdx: questionIdx + 1,
questionIdx: elementIdx + 1,
filterComboBoxValue: filterComboBoxValue?.toString() ?? "",
filterValue,
});
} else if (filterComboBoxValue === undefined) {
return t("environments.surveys.summary.added_filter_for_responses_where_answer_to_question_is_skipped", {
questionIdx: questionIdx + 1,
questionIdx: elementIdx + 1,
});
} else {
return t("environments.surveys.summary.added_filter_for_responses_where_answer_to_question", {
questionIdx: questionIdx + 1,
questionIdx: elementIdx + 1,
filterComboBoxValue: Array.isArray(filterComboBoxValue)
? filterComboBoxValue.join(",")
: filterComboBoxValue,

View File

@@ -4,8 +4,8 @@ import clsx from "clsx";
import { ChevronDown, ChevronUp, X } from "lucide-react";
import { useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ElementsComboBox";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { useClickOutside } from "@/lib/utils/hooks/useClickOutside";
import { Button } from "@/modules/ui/components/button";
@@ -25,20 +25,20 @@ import {
} from "@/modules/ui/components/dropdown-menu";
import { Input } from "@/modules/ui/components/input";
type QuestionFilterComboBoxProps = {
type ElementFilterComboBoxProps = {
filterOptions: string[] | undefined;
filterComboBoxOptions: string[] | undefined;
filterValue: string | undefined;
filterComboBoxValue: string | string[] | undefined;
onChangeFilterValue: (o: string) => void;
onChangeFilterComboBoxValue: (o: string | string[]) => void;
type?: TSurveyQuestionTypeEnum | Omit<OptionsType, OptionsType.QUESTIONS>;
type?: TSurveyElementTypeEnum | Omit<OptionsType, OptionsType.ELEMENTS>;
handleRemoveMultiSelect: (value: string[]) => void;
disabled?: boolean;
fieldId?: string;
};
export const QuestionFilterComboBox = ({
export const ElementFilterComboBox = ({
filterComboBoxOptions,
filterComboBoxValue,
filterOptions,
@@ -49,7 +49,7 @@ export const QuestionFilterComboBox = ({
handleRemoveMultiSelect,
disabled = false,
fieldId,
}: QuestionFilterComboBoxProps) => {
}: ElementFilterComboBoxProps) => {
const [open, setOpen] = useState(false);
const commandRef = useRef(null);
const [searchQuery, setSearchQuery] = useState("");
@@ -62,10 +62,10 @@ export const QuestionFilterComboBox = ({
// Check if multiple selection is allowed
const isMultiple = useMemo(
() =>
type === TSurveyQuestionTypeEnum.MultipleChoiceMulti ||
type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ||
type === TSurveyQuestionTypeEnum.PictureSelection ||
(type === TSurveyQuestionTypeEnum.NPS && filterValue === "Includes either"),
type === TSurveyElementTypeEnum.MultipleChoiceMulti ||
type === TSurveyElementTypeEnum.MultipleChoiceSingle ||
type === TSurveyElementTypeEnum.PictureSelection ||
(type === TSurveyElementTypeEnum.NPS && filterValue === "Includes either"),
[type, filterValue]
);
@@ -81,7 +81,7 @@ export const QuestionFilterComboBox = ({
// Disable combo box for NPS/Rating when Submitted/Skipped
const isDisabledComboBox =
(type === TSurveyQuestionTypeEnum.NPS || type === TSurveyQuestionTypeEnum.Rating) &&
(type === TSurveyElementTypeEnum.NPS || type === TSurveyElementTypeEnum.Rating) &&
(filterValue === "Submitted" || filterValue === "Skipped");
// Check if this is a text input field (URL meta field)

View File

@@ -44,7 +44,7 @@ import {
import { NetPromoterScoreIcon } from "@/modules/ui/components/icons";
export enum OptionsType {
QUESTIONS = "Questions",
ELEMENTS = "Elements",
TAGS = "Tags",
ATTRIBUTES = "Attributes",
OTHERS = "Other Filters",
@@ -53,25 +53,25 @@ export enum OptionsType {
QUOTAS = "Quotas",
}
export type QuestionOption = {
export type ElementOption = {
label: string;
questionType?: TSurveyElementTypeEnum;
elementType?: TSurveyElementTypeEnum;
type: OptionsType;
id: string;
};
export type QuestionOptions = {
export type ElementOptions = {
header: OptionsType;
option: QuestionOption[];
option: ElementOption[];
};
interface QuestionComboBoxProps {
options: QuestionOptions[];
selected: Partial<QuestionOption>;
onChangeValue: (option: QuestionOption) => void;
interface ElementComboBoxProps {
options: ElementOptions[];
selected: Partial<ElementOption>;
onChangeValue: (option: ElementOption) => void;
}
const questionIcons = {
// questions
const elementIcons = {
// elements
[TSurveyElementTypeEnum.OpenText]: MessageSquareTextIcon,
[TSurveyElementTypeEnum.Rating]: StarIcon,
[TSurveyElementTypeEnum.CTA]: MousePointerClickIcon,
@@ -111,14 +111,14 @@ const questionIcons = {
};
const getIcon = (type: string) => {
const IconComponent = questionIcons[type];
const IconComponent = elementIcons[type];
return IconComponent ? <IconComponent className="h-5 w-5" strokeWidth={1.5} /> : null;
};
const getIconBackground = (type: OptionsType | string): string => {
const backgroundMap: Record<string, string> = {
[OptionsType.ATTRIBUTES]: "bg-indigo-500",
[OptionsType.QUESTIONS]: "bg-brand-dark",
[OptionsType.ELEMENTS]: "bg-brand-dark",
[OptionsType.TAGS]: "bg-indigo-500",
[OptionsType.QUOTAS]: "bg-slate-500",
};
@@ -130,10 +130,10 @@ const getLabelClassName = (type: OptionsType | string, label?: string): string =
return label === "os" || label === "url" ? "uppercase" : "capitalize";
};
export const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOption>) => {
export const SelectedCommandItem = ({ label, elementType, type }: Partial<ElementOption>) => {
const getDisplayIcon = () => {
if (!type) return null;
if (type === OptionsType.QUESTIONS && questionType) return getIcon(questionType);
if (type === OptionsType.ELEMENTS && elementType) return getIcon(elementType);
if (type === OptionsType.ATTRIBUTES) return getIcon(OptionsType.ATTRIBUTES);
if (type === OptionsType.HIDDEN_FIELDS) return getIcon(OptionsType.HIDDEN_FIELDS);
if ([OptionsType.META, OptionsType.OTHERS].includes(type) && label) return getIcon(label);
@@ -158,7 +158,7 @@ export const SelectedCommandItem = ({ label, questionType, type }: Partial<Quest
);
};
export const QuestionsComboBox = ({ options, selected, onChangeValue }: QuestionComboBoxProps) => {
export const ElementsComboBox = ({ options, selected, onChangeValue }: ElementComboBoxProps) => {
const [open, setOpen] = useState(false);
const { t } = useTranslation();
const commandRef = useRef(null);

View File

@@ -12,8 +12,8 @@ import {
useResponseFilter,
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
import { getSurveyFilterDataAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions";
import { QuestionFilterComboBox } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox";
import { generateQuestionAndFilterOptions } from "@/app/lib/surveys/surveys";
import { ElementFilterComboBox } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ElementFilterComboBox";
import { generateElementAndFilterOptions } from "@/app/lib/surveys/surveys";
import { Button } from "@/modules/ui/components/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/modules/ui/components/popover";
import {
@@ -23,9 +23,9 @@ import {
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
import { OptionsType, QuestionOption, QuestionsComboBox } from "./QuestionsComboBox";
import { ElementOption, ElementsComboBox, OptionsType } from "./ElementsComboBox";
export type QuestionFilterOptions = {
export type ElementFilterOptions = {
type: TSurveyElementTypeEnum | "Attributes" | "Tags" | "Languages" | "Quotas";
filterOptions: string[];
filterComboBoxOptions: string[];
@@ -79,7 +79,7 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
if (!surveyFilterData?.data) return;
const { attributes, meta, environmentTags, hiddenFields, quotas } = surveyFilterData.data;
const { questionFilterOptions, questionOptions } = generateQuestionAndFilterOptions(
const { elementFilterOptions, elementOptions } = generateElementAndFilterOptions(
survey,
environmentTags,
attributes,
@@ -87,33 +87,33 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
hiddenFields,
quotas
);
setSelectedOptions({ questionFilterOptions, questionOptions });
setSelectedOptions({ elementFilterOptions: elementFilterOptions, elementOptions: elementOptions });
}
};
handleInitialData();
}, [isOpen, setSelectedOptions, survey]);
const handleOnChangeQuestionComboBoxValue = (value: QuestionOption, index: number) => {
if (filterValue.filter[index].questionType) {
const handleOnChangeElementComboBoxValue = (value: ElementOption, index: number) => {
if (filterValue.filter[index].elementType) {
// Create a new array and copy existing values from SelectedFilter
filterValue.filter[index] = {
questionType: value,
elementType: value,
filterType: {
filterComboBoxValue: undefined,
filterValue: selectedOptions.questionFilterOptions.find(
(q) => q.type === value.type || q.type === value.questionType
filterValue: selectedOptions.elementFilterOptions.find(
(q) => q.type === value.type || q.type === value.elementType
)?.filterOptions[0],
},
};
setFilterValue({ filter: [...filterValue.filter], responseStatus: filterValue.responseStatus });
} else {
// Update the existing value at the specified index
filterValue.filter[index].questionType = value;
filterValue.filter[index].elementType = value;
filterValue.filter[index].filterType = {
filterComboBoxValue: undefined,
filterValue: selectedOptions.questionFilterOptions.find(
(q) => q.type === value.type || q.type === value.questionType
filterValue: selectedOptions.elementFilterOptions.find(
(q) => q.type === value.type || q.type === value.elementType
)?.filterOptions[0],
};
setFilterValue({ ...filterValue });
@@ -124,8 +124,8 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
const clearItem = () => {
setFilterValue({
filter: filterValue.filter.filter((s) => {
// keep the filter if questionType is selected and filterComboBoxValue is selected
return s.questionType.hasOwnProperty("label") && s.filterType.filterComboBoxValue?.length;
// keep the filter if elementType is selected and filterComboBoxValue is selected
return s.elementType.hasOwnProperty("label") && s.filterType.filterComboBoxValue?.length;
}),
responseStatus: filterValue.responseStatus,
});
@@ -145,7 +145,7 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
filter: [
...filterValue.filter,
{
questionType: {},
elementType: {},
filterType: { filterComboBoxValue: undefined, filterValue: undefined },
},
],
@@ -197,10 +197,10 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
};
// remove the filter which has already been selected
const questionComboBoxOptions = selectedOptions.questionOptions.map((q) => {
const elementComboBoxOptions = selectedOptions.elementOptions.map((q) => {
return {
...q,
option: q.option.filter((o) => !filterValue.filter.some((f) => f?.questionType?.id === o?.id)),
option: q.option.filter((o) => !filterValue.filter.some((f) => f?.elementType?.id === o?.id)),
};
});
@@ -261,41 +261,41 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
<div className="flex w-full flex-wrap gap-3 md:flex-nowrap">
<div
className="grid w-full grid-cols-1 items-center gap-3 md:grid-cols-2"
key={`${s.questionType.id}-${i}-${s.questionType.label}`}>
<QuestionsComboBox
key={`${s.questionType.label}-${i}-${s.questionType.id}`}
options={questionComboBoxOptions}
selected={s.questionType}
onChangeValue={(value) => handleOnChangeQuestionComboBoxValue(value, i)}
key={`${s.elementType.id}-${i}-${s.elementType.label}`}>
<ElementsComboBox
key={`${s.elementType.label}-${i}-${s.elementType.id}`}
options={elementComboBoxOptions}
selected={s.elementType}
onChangeValue={(value) => handleOnChangeElementComboBoxValue(value, i)}
/>
<QuestionFilterComboBox
key={`${s.questionType.id}-${i}`}
<ElementFilterComboBox
key={`${s.elementType.id}-${i}`}
filterOptions={
selectedOptions.questionFilterOptions.find(
selectedOptions.elementFilterOptions.find(
(q) =>
(q.type === s.questionType.questionType || q.type === s.questionType.type) &&
q.id === s.questionType.id
(q.type === s.elementType.elementType || q.type === s.elementType.type) &&
q.id === s.elementType.id
)?.filterOptions
}
filterComboBoxOptions={
selectedOptions.questionFilterOptions.find(
selectedOptions.elementFilterOptions.find(
(q) =>
(q.type === s.questionType.questionType || q.type === s.questionType.type) &&
q.id === s.questionType.id
(q.type === s.elementType.elementType || q.type === s.elementType.type) &&
q.id === s.elementType.id
)?.filterComboBoxOptions
}
filterValue={filterValue.filter[i].filterType.filterValue}
filterComboBoxValue={filterValue.filter[i].filterType.filterComboBoxValue}
type={
s?.questionType?.type === OptionsType.QUESTIONS
? s?.questionType?.questionType
: s?.questionType?.type
s?.elementType?.type === OptionsType.ELEMENTS
? s?.elementType?.elementType
: s?.elementType?.type
}
fieldId={s?.questionType?.id}
fieldId={s?.elementType?.id}
handleRemoveMultiSelect={(value) => handleRemoveMultiSelect(value, i)}
onChangeFilterComboBoxValue={(value) => handleOnChangeFilterComboBoxValue(value, i)}
onChangeFilterValue={(value) => handleOnChangeFilterValue(value, i)}
disabled={!s?.questionType?.label}
disabled={!s?.elementType?.label}
/>
</div>
<div className="flex w-full items-center justify-end gap-1 md:w-auto">

View File

@@ -8,7 +8,7 @@ import { withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
import { sendToPipeline } from "@/app/lib/pipelines";
import { getResponse } from "@/lib/response/service";
import { getSurvey } from "@/lib/survey/service";
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/question";
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers";
import { validateFileUploads } from "@/modules/storage/utils";
import { updateResponseWithQuotaEvaluation } from "./lib/response";

View File

@@ -11,7 +11,7 @@ import { sendToPipeline } from "@/app/lib/pipelines";
import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
import { getSurvey } from "@/lib/survey/service";
import { getElementsFromBlocks } from "@/lib/survey/utils";
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/question";
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers";
import { createResponseWithQuotaEvaluation } from "./lib/response";

View File

@@ -9,8 +9,8 @@ import {
DateRange,
SelectedFilterValue,
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
import { generateQuestionAndFilterOptions, getFormattedFilters, getTodayDate } from "./surveys";
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ElementsComboBox";
import { generateElementAndFilterOptions, getFormattedFilters, getTodayDate } from "./surveys";
describe("surveys", () => {
afterEach(() => {
@@ -45,12 +45,12 @@ describe("surveys", () => {
status: "draft",
} as unknown as TSurvey;
const result = generateQuestionAndFilterOptions(survey, undefined, {}, {}, {}, []);
const result = generateElementAndFilterOptions(survey, undefined, {}, {}, {}, []);
expect(result.questionOptions.length).toBeGreaterThan(0);
expect(result.questionOptions[0].header).toBe(OptionsType.QUESTIONS);
expect(result.questionFilterOptions.length).toBe(1);
expect(result.questionFilterOptions[0].id).toBe("q1");
expect(result.elementOptions.length).toBeGreaterThan(0);
expect(result.elementOptions[0].header).toBe(OptionsType.ELEMENTS);
expect(result.elementFilterOptions.length).toBe(1);
expect(result.elementFilterOptions[0].id).toBe("q1");
});
test("should include tags in options when provided", () => {
@@ -69,9 +69,9 @@ describe("surveys", () => {
{ id: "tag1", name: "Tag 1", environmentId: "env1", createdAt: new Date(), updatedAt: new Date() },
];
const result = generateQuestionAndFilterOptions(survey, tags, {}, {}, {}, []);
const result = generateElementAndFilterOptions(survey, tags, {}, {}, {}, []);
const tagsHeader = result.questionOptions.find((opt) => opt.header === OptionsType.TAGS);
const tagsHeader = result.elementOptions.find((opt) => opt.header === OptionsType.TAGS);
expect(tagsHeader).toBeDefined();
expect(tagsHeader?.option.length).toBe(1);
expect(tagsHeader?.option[0].label).toBe("Tag 1");
@@ -93,9 +93,9 @@ describe("surveys", () => {
role: ["admin", "user"],
};
const result = generateQuestionAndFilterOptions(survey, undefined, attributes, {}, {}, []);
const result = generateElementAndFilterOptions(survey, undefined, attributes, {}, {}, []);
const attributesHeader = result.questionOptions.find((opt) => opt.header === OptionsType.ATTRIBUTES);
const attributesHeader = result.elementOptions.find((opt) => opt.header === OptionsType.ATTRIBUTES);
expect(attributesHeader).toBeDefined();
expect(attributesHeader?.option.length).toBe(1);
expect(attributesHeader?.option[0].label).toBe("role");
@@ -117,9 +117,9 @@ describe("surveys", () => {
source: ["web", "mobile"],
};
const result = generateQuestionAndFilterOptions(survey, undefined, {}, meta, {}, []);
const result = generateElementAndFilterOptions(survey, undefined, {}, meta, {}, []);
const metaHeader = result.questionOptions.find((opt) => opt.header === OptionsType.META);
const metaHeader = result.elementOptions.find((opt) => opt.header === OptionsType.META);
expect(metaHeader).toBeDefined();
expect(metaHeader?.option.length).toBe(1);
expect(metaHeader?.option[0].label).toBe("source");
@@ -141,9 +141,9 @@ describe("surveys", () => {
segment: ["free", "paid"],
};
const result = generateQuestionAndFilterOptions(survey, undefined, {}, {}, hiddenFields, []);
const result = generateElementAndFilterOptions(survey, undefined, {}, {}, hiddenFields, []);
const hiddenFieldsHeader = result.questionOptions.find(
const hiddenFieldsHeader = result.elementOptions.find(
(opt) => opt.header === OptionsType.HIDDEN_FIELDS
);
expect(hiddenFieldsHeader).toBeDefined();
@@ -164,9 +164,9 @@ describe("surveys", () => {
languages: [{ language: { code: "en" } as unknown as TLanguage } as unknown as TSurveyLanguage],
} as unknown as TSurvey;
const result = generateQuestionAndFilterOptions(survey, undefined, {}, {}, {}, []);
const result = generateElementAndFilterOptions(survey, undefined, {}, {}, {}, []);
const othersHeader = result.questionOptions.find((opt) => opt.header === OptionsType.OTHERS);
const othersHeader = result.elementOptions.find((opt) => opt.header === OptionsType.OTHERS);
expect(othersHeader).toBeDefined();
expect(othersHeader?.option.some((o) => o.label === "Language")).toBeTruthy();
});
@@ -262,13 +262,13 @@ describe("surveys", () => {
status: "draft",
} as unknown as TSurvey;
const result = generateQuestionAndFilterOptions(survey, undefined, {}, {}, {}, []);
const result = generateElementAndFilterOptions(survey, undefined, {}, {}, {}, []);
expect(result.questionFilterOptions.length).toBe(8);
expect(result.questionFilterOptions.some((o) => o.id === "q1")).toBeTruthy();
expect(result.questionFilterOptions.some((o) => o.id === "q2")).toBeTruthy();
expect(result.questionFilterOptions.some((o) => o.id === "q7")).toBeTruthy();
expect(result.questionFilterOptions.some((o) => o.id === "q8")).toBeTruthy();
expect(result.elementFilterOptions.length).toBe(8);
expect(result.elementFilterOptions.some((o) => o.id === "q1")).toBeTruthy();
expect(result.elementFilterOptions.some((o) => o.id === "q2")).toBeTruthy();
expect(result.elementFilterOptions.some((o) => o.id === "q7")).toBeTruthy();
expect(result.elementFilterOptions.some((o) => o.id === "q8")).toBeTruthy();
});
test("should provide extended filter options for URL meta field", () => {
@@ -288,10 +288,10 @@ describe("surveys", () => {
source: ["web", "mobile"],
};
const result = generateQuestionAndFilterOptions(survey, undefined, {}, meta, {}, []);
const result = generateElementAndFilterOptions(survey, undefined, {}, meta, {}, []);
const urlFilterOption = result.questionFilterOptions.find((o) => o.id === "url");
const sourceFilterOption = result.questionFilterOptions.find((o) => o.id === "source");
const urlFilterOption = result.elementFilterOptions.find((o) => o.id === "url");
const sourceFilterOption = result.elementFilterOptions.find((o) => o.id === "source");
expect(urlFilterOption).toBeDefined();
expect(urlFilterOption?.filterOptions).toEqual([

View File

@@ -15,11 +15,11 @@ import {
SelectedFilterValue,
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
import {
ElementOption,
ElementOptions,
OptionsType,
QuestionOption,
QuestionOptions,
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
import { QuestionFilterOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter";
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ElementsComboBox";
import { ElementFilterOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { recallToHeadline } from "@/lib/utils/recall";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
@@ -65,8 +65,7 @@ const META_OP_MAP = {
"Does not end with": "doesNotEndWith",
} as const;
// creating the options for the filtering to be selected there are 4 types questions, attributes, tags and metadata
export const generateQuestionAndFilterOptions = (
export const generateElementAndFilterOptions = (
survey: TSurvey,
environmentTags: TTag[] | undefined,
attributes: TSurveyContactAttributes,
@@ -74,39 +73,39 @@ export const generateQuestionAndFilterOptions = (
hiddenFields: TResponseHiddenFieldsFilter,
quotas: TSurveyQuota[]
): {
questionOptions: QuestionOptions[];
questionFilterOptions: QuestionFilterOptions[];
elementOptions: ElementOptions[];
elementFilterOptions: ElementFilterOptions[];
} => {
let questionOptions: QuestionOptions[] = [];
let questionFilterOptions: any = [];
let elementOptions: ElementOptions[] = [];
let elementFilterOptions: any = [];
let questionsOptions: any = [];
const questions = getElementsFromBlocks(survey.blocks);
let elementsOptions: any = [];
const elements = getElementsFromBlocks(survey.blocks);
questions.forEach((q) => {
elements.forEach((q) => {
if (Object.keys(conditionOptions).includes(q.type)) {
questionsOptions.push({
elementsOptions.push({
label: getTextContent(
getLocalizedValue(recallToHeadline(q.headline, survey, false, "default"), "default")
),
questionType: q.type,
type: OptionsType.QUESTIONS,
elementType: q.type,
type: OptionsType.ELEMENTS,
id: q.id,
});
}
});
questionOptions = [...questionOptions, { header: OptionsType.QUESTIONS, option: questionsOptions }];
questions.forEach((q) => {
elementOptions = [...elementOptions, { header: OptionsType.ELEMENTS, option: elementsOptions }];
elements.forEach((q) => {
if (Object.keys(conditionOptions).includes(q.type)) {
if (q.type === TSurveyElementTypeEnum.MultipleChoiceSingle) {
questionFilterOptions.push({
elementFilterOptions.push({
type: q.type,
filterOptions: conditionOptions[q.type],
filterComboBoxOptions: q?.choices ? q?.choices?.map((c) => c?.label) : [""],
id: q.id,
});
} else if (q.type === TSurveyElementTypeEnum.MultipleChoiceMulti) {
questionFilterOptions.push({
elementFilterOptions.push({
type: q.type,
filterOptions: conditionOptions[q.type],
filterComboBoxOptions: q?.choices
@@ -115,21 +114,21 @@ export const generateQuestionAndFilterOptions = (
id: q.id,
});
} else if (q.type === TSurveyElementTypeEnum.PictureSelection) {
questionFilterOptions.push({
elementFilterOptions.push({
type: q.type,
filterOptions: conditionOptions[q.type],
filterComboBoxOptions: q?.choices ? q?.choices?.map((_, idx) => `Picture ${idx + 1}`) : [""],
id: q.id,
});
} else if (q.type === TSurveyElementTypeEnum.Matrix) {
questionFilterOptions.push({
elementFilterOptions.push({
type: q.type,
filterOptions: q.rows.flatMap((row) => Object.values(row)),
filterComboBoxOptions: q.columns.flatMap((column) => Object.values(column)),
id: q.id,
});
} else {
questionFilterOptions.push({
elementFilterOptions.push({
type: q.type,
filterOptions: conditionOptions[q.type],
filterComboBoxOptions: filterOptions[q.type],
@@ -143,9 +142,9 @@ export const generateQuestionAndFilterOptions = (
return { label: t.name, type: OptionsType.TAGS, id: t.id };
});
if (tagsOptions && tagsOptions?.length > 0) {
questionOptions = [...questionOptions, { header: OptionsType.TAGS, option: tagsOptions }];
elementOptions = [...elementOptions, { header: OptionsType.TAGS, option: tagsOptions }];
environmentTags?.forEach((t) => {
questionFilterOptions.push({
elementFilterOptions.push({
type: "Tags",
filterOptions: conditionOptions.tags,
filterComboBoxOptions: filterOptions.tags,
@@ -155,8 +154,8 @@ export const generateQuestionAndFilterOptions = (
}
if (attributes) {
questionOptions = [
...questionOptions,
elementOptions = [
...elementOptions,
{
header: OptionsType.ATTRIBUTES,
option: Object.keys(attributes).map((a) => {
@@ -165,7 +164,7 @@ export const generateQuestionAndFilterOptions = (
},
];
Object.keys(attributes).forEach((a) => {
questionFilterOptions.push({
elementFilterOptions.push({
type: "Attributes",
filterOptions: conditionOptions.userAttributes,
filterComboBoxOptions: attributes[a],
@@ -175,8 +174,8 @@ export const generateQuestionAndFilterOptions = (
}
if (meta) {
questionOptions = [
...questionOptions,
elementOptions = [
...elementOptions,
{
header: OptionsType.META,
option: Object.keys(meta).map((m) => {
@@ -185,7 +184,7 @@ export const generateQuestionAndFilterOptions = (
},
];
Object.keys(meta).forEach((m) => {
questionFilterOptions.push({
elementFilterOptions.push({
type: "Meta",
filterOptions: m === "url" ? Object.keys(META_OP_MAP) : ["Equals", "Not equals"],
filterComboBoxOptions: meta[m],
@@ -195,8 +194,8 @@ export const generateQuestionAndFilterOptions = (
}
if (hiddenFields) {
questionOptions = [
...questionOptions,
elementOptions = [
...elementOptions,
{
header: OptionsType.HIDDEN_FIELDS,
option: Object.keys(hiddenFields).map((hiddenField) => {
@@ -205,7 +204,7 @@ export const generateQuestionAndFilterOptions = (
},
];
Object.keys(hiddenFields).forEach((hiddenField) => {
questionFilterOptions.push({
elementFilterOptions.push({
type: "Hidden Fields",
filterOptions: ["Equals", "Not equals"],
filterComboBoxOptions: hiddenFields[hiddenField],
@@ -214,29 +213,29 @@ export const generateQuestionAndFilterOptions = (
});
}
let languageQuestion: QuestionOption[] = [];
let languageElement: ElementOption[] = [];
//can be extended to include more properties
if (survey.languages?.length > 0) {
languageQuestion.push({ label: "Language", type: OptionsType.OTHERS, id: "language" });
languageElement.push({ label: "Language", type: OptionsType.OTHERS, id: "language" });
const languageOptions = survey.languages.map((sl) => sl.language.code);
questionFilterOptions.push({
elementFilterOptions.push({
type: OptionsType.OTHERS,
filterOptions: conditionOptions.languages,
filterComboBoxOptions: languageOptions,
id: "language",
});
}
questionOptions = [...questionOptions, { header: OptionsType.OTHERS, option: languageQuestion }];
elementOptions = [...elementOptions, { header: OptionsType.OTHERS, option: languageElement }];
if (quotas.length > 0) {
const quotaOptions = quotas.map((quota) => {
return { label: quota.name, type: OptionsType.QUOTAS, id: quota.id };
});
questionOptions = [...questionOptions, { header: OptionsType.QUOTAS, option: quotaOptions }];
elementOptions = [...elementOptions, { header: OptionsType.QUOTAS, option: quotaOptions }];
quotas.forEach((quota) => {
questionFilterOptions.push({
elementFilterOptions.push({
type: "Quotas",
filterOptions: ["Status"],
filterComboBoxOptions: ["Screened in", "Screened out (overquota)", "Screened out (not in quota)"],
@@ -245,7 +244,7 @@ export const generateQuestionAndFilterOptions = (
});
}
return { questionOptions: [...questionOptions], questionFilterOptions: [...questionFilterOptions] };
return { elementOptions: [...elementOptions], elementFilterOptions: [...elementFilterOptions] };
};
// get the formatted filter expression to fetch filtered responses
@@ -256,7 +255,7 @@ export const getFormattedFilters = (
): TResponseFilterCriteria => {
const filters: TResponseFilterCriteria = {};
const questions: FilterValue[] = [];
const elements: FilterValue[] = [];
const tags: FilterValue[] = [];
const attributes: FilterValue[] = [];
const others: FilterValue[] = [];
@@ -265,19 +264,19 @@ export const getFormattedFilters = (
const quotas: FilterValue[] = [];
selectedFilter.filter.forEach((filter) => {
if (filter.questionType?.type === "Questions") {
questions.push(filter);
} else if (filter.questionType?.type === "Tags") {
if (filter.elementType?.type === "Elements") {
elements.push(filter);
} else if (filter.elementType?.type === "Tags") {
tags.push(filter);
} else if (filter.questionType?.type === "Attributes") {
} else if (filter.elementType?.type === "Attributes") {
attributes.push(filter);
} else if (filter.questionType?.type === "Other Filters") {
} else if (filter.elementType?.type === "Other Filters") {
others.push(filter);
} else if (filter.questionType?.type === "Meta") {
} else if (filter.elementType?.type === "Meta") {
meta.push(filter);
} else if (filter.questionType?.type === "Hidden Fields") {
} else if (filter.elementType?.type === "Hidden Fields") {
hiddenFields.push(filter);
} else if (filter.questionType?.type === "Quotas") {
} else if (filter.elementType?.type === "Quotas") {
quotas.push(filter);
}
});
@@ -305,28 +304,27 @@ export const getFormattedFilters = (
};
tags.forEach((tag) => {
if (tag.filterType.filterComboBoxValue === "Applied") {
filters.tags?.applied?.push(tag.questionType.label ?? "");
filters.tags?.applied?.push(tag.elementType.label ?? "");
} else {
filters.tags?.notApplied?.push(tag.questionType.label ?? "");
filters.tags?.notApplied?.push(tag.elementType.label ?? "");
}
});
}
// for questions
if (questions.length) {
const surveyQuestions = getElementsFromBlocks(survey.blocks);
questions.forEach(({ filterType, questionType }) => {
if (elements.length) {
const surveyElements = getElementsFromBlocks(survey.blocks);
elements.forEach(({ filterType, elementType }) => {
if (!filters.data) filters.data = {};
switch (questionType.questionType) {
switch (elementType.elementType) {
case TSurveyElementTypeEnum.OpenText:
case TSurveyElementTypeEnum.Address:
case TSurveyElementTypeEnum.ContactInfo: {
if (filterType.filterComboBoxValue === "Filled out") {
filters.data[questionType.id ?? ""] = {
filters.data[elementType.id ?? ""] = {
op: "filledOut",
};
} else if (filterType.filterComboBoxValue === "Skipped") {
filters.data[questionType.id ?? ""] = {
filters.data[elementType.id ?? ""] = {
op: "skipped",
};
}
@@ -334,11 +332,11 @@ export const getFormattedFilters = (
}
case TSurveyElementTypeEnum.Ranking: {
if (filterType.filterComboBoxValue === "Filled out") {
filters.data[questionType.id ?? ""] = {
filters.data[elementType.id ?? ""] = {
op: "submitted",
};
} else if (filterType.filterComboBoxValue === "Skipped") {
filters.data[questionType.id ?? ""] = {
filters.data[elementType.id ?? ""] = {
op: "skipped",
};
}
@@ -347,12 +345,12 @@ export const getFormattedFilters = (
case TSurveyElementTypeEnum.MultipleChoiceSingle:
case TSurveyElementTypeEnum.MultipleChoiceMulti: {
if (filterType.filterValue === "Includes either") {
filters.data[questionType.id ?? ""] = {
filters.data[elementType.id ?? ""] = {
op: "includesOne",
value: filterType.filterComboBoxValue as string[],
};
} else if (filterType.filterValue === "Includes all") {
filters.data[questionType.id ?? ""] = {
filters.data[elementType.id ?? ""] = {
op: "includesAll",
value: filterType.filterComboBoxValue as string[],
};
@@ -362,30 +360,30 @@ export const getFormattedFilters = (
case TSurveyElementTypeEnum.NPS:
case TSurveyElementTypeEnum.Rating: {
if (filterType.filterValue === "Is equal to") {
filters.data[questionType.id ?? ""] = {
filters.data[elementType.id ?? ""] = {
op: "equals",
value: parseInt(filterType.filterComboBoxValue as string),
};
} else if (filterType.filterValue === "Is less than") {
filters.data[questionType.id ?? ""] = {
filters.data[elementType.id ?? ""] = {
op: "lessThan",
value: parseInt(filterType.filterComboBoxValue as string),
};
} else if (filterType.filterValue === "Is more than") {
filters.data[questionType.id ?? ""] = {
filters.data[elementType.id ?? ""] = {
op: "greaterThan",
value: parseInt(filterType.filterComboBoxValue as string),
};
} else if (filterType.filterValue === "Submitted") {
filters.data[questionType.id ?? ""] = {
filters.data[elementType.id ?? ""] = {
op: "submitted",
};
} else if (filterType.filterValue === "Skipped") {
filters.data[questionType.id ?? ""] = {
filters.data[elementType.id ?? ""] = {
op: "skipped",
};
} else if (filterType.filterValue === "Includes either") {
filters.data[questionType.id ?? ""] = {
filters.data[elementType.id ?? ""] = {
op: "includesOne",
value: (filterType.filterComboBoxValue as string[]).map((value) => parseInt(value)),
};
@@ -394,11 +392,11 @@ export const getFormattedFilters = (
}
case TSurveyElementTypeEnum.CTA: {
if (filterType.filterComboBoxValue === "Clicked") {
filters.data[questionType.id ?? ""] = {
filters.data[elementType.id ?? ""] = {
op: "clicked",
};
} else if (filterType.filterComboBoxValue === "Dismissed") {
filters.data[questionType.id ?? ""] = {
filters.data[elementType.id ?? ""] = {
op: "skipped",
};
}
@@ -406,22 +404,22 @@ export const getFormattedFilters = (
}
case TSurveyElementTypeEnum.Consent: {
if (filterType.filterComboBoxValue === "Accepted") {
filters.data[questionType.id ?? ""] = {
filters.data[elementType.id ?? ""] = {
op: "accepted",
};
} else if (filterType.filterComboBoxValue === "Dismissed") {
filters.data[questionType.id ?? ""] = {
filters.data[elementType.id ?? ""] = {
op: "skipped",
};
}
break;
}
case TSurveyElementTypeEnum.PictureSelection: {
const questionId = questionType.id ?? "";
const question = surveyQuestions.find((q) => q.id === questionId);
const elementId = elementType.id ?? "";
const element = surveyElements.find((q) => q.id === elementId);
if (
question?.type !== TSurveyElementTypeEnum.PictureSelection ||
element?.type !== TSurveyElementTypeEnum.PictureSelection ||
!Array.isArray(filterType.filterComboBoxValue)
) {
return;
@@ -429,16 +427,16 @@ export const getFormattedFilters = (
const selectedOptions = filterType.filterComboBoxValue.map((option) => {
const index = parseInt(option.split(" ")[1]);
return question?.choices[index - 1].id;
return element?.choices[index - 1].id;
});
if (filterType.filterValue === "Includes all") {
filters.data[questionId] = {
filters.data[elementId] = {
op: "includesAll",
value: selectedOptions,
};
} else if (filterType.filterValue === "Includes either") {
filters.data[questionId] = {
filters.data[elementId] = {
op: "includesOne",
value: selectedOptions,
};
@@ -451,7 +449,7 @@ export const getFormattedFilters = (
filterType.filterComboBoxValue &&
typeof filterType.filterComboBoxValue === "string"
) {
filters.data[questionType.id ?? ""] = {
filters.data[elementType.id ?? ""] = {
op: "matrix",
value: { [filterType.filterValue]: filterType.filterComboBoxValue },
};
@@ -464,16 +462,16 @@ export const getFormattedFilters = (
// for hidden fields
if (hiddenFields.length) {
hiddenFields.forEach(({ filterType, questionType }) => {
hiddenFields.forEach(({ filterType, elementType }) => {
if (!filters.data) filters.data = {};
if (!filterType.filterComboBoxValue) return;
if (filterType.filterValue === "Equals") {
filters.data[questionType.label ?? ""] = {
filters.data[elementType.label ?? ""] = {
op: "equals",
value: filterType.filterComboBoxValue as string,
};
} else if (filterType.filterValue === "Not equals") {
filters.data[questionType.label ?? ""] = {
filters.data[elementType.label ?? ""] = {
op: "notEquals",
value: filterType.filterComboBoxValue as string,
};
@@ -483,16 +481,16 @@ export const getFormattedFilters = (
// for attributes
if (attributes.length) {
attributes.forEach(({ filterType, questionType }) => {
attributes.forEach(({ filterType, elementType }) => {
if (!filters.contactAttributes) filters.contactAttributes = {};
if (!filterType.filterComboBoxValue) return;
if (filterType.filterValue === "Equals") {
filters.contactAttributes[questionType.label ?? ""] = {
filters.contactAttributes[elementType.label ?? ""] = {
op: "equals",
value: filterType.filterComboBoxValue as string,
};
} else if (filterType.filterValue === "Not equals") {
filters.contactAttributes[questionType.label ?? ""] = {
filters.contactAttributes[elementType.label ?? ""] = {
op: "notEquals",
value: filterType.filterComboBoxValue as string,
};
@@ -502,16 +500,16 @@ export const getFormattedFilters = (
// for others
if (others.length) {
others.forEach(({ filterType, questionType }) => {
others.forEach(({ filterType, elementType }) => {
if (!filters.others) filters.others = {};
if (!filterType.filterComboBoxValue) return;
if (filterType.filterValue === "Equals") {
filters.others[questionType.label ?? ""] = {
filters.others[elementType.label ?? ""] = {
op: "equals",
value: filterType.filterComboBoxValue as string,
};
} else if (filterType.filterValue === "Not equals") {
filters.others[questionType.label ?? ""] = {
filters.others[elementType.label ?? ""] = {
op: "notEquals",
value: filterType.filterComboBoxValue as string,
};
@@ -521,7 +519,7 @@ export const getFormattedFilters = (
// for meta
if (meta.length) {
meta.forEach(({ filterType, questionType }) => {
meta.forEach(({ filterType, elementType }) => {
if (!filters.meta) filters.meta = {};
// For text input cases (URL filtering)
@@ -529,25 +527,25 @@ export const getFormattedFilters = (
const value = filterType.filterComboBoxValue.trim();
const op = META_OP_MAP[filterType.filterValue as keyof typeof META_OP_MAP];
if (op) {
filters.meta[questionType.label ?? ""] = { op, value };
filters.meta[elementType.label ?? ""] = { op, value };
}
}
// For dropdown/select cases (existing metadata fields)
else if (Array.isArray(filterType.filterComboBoxValue) && filterType.filterComboBoxValue.length > 0) {
const value = filterType.filterComboBoxValue[0]; // Take first selected value
if (filterType.filterValue === "Equals") {
filters.meta[questionType.label ?? ""] = { op: "equals", value };
filters.meta[elementType.label ?? ""] = { op: "equals", value };
} else if (filterType.filterValue === "Not equals") {
filters.meta[questionType.label ?? ""] = { op: "notEquals", value };
filters.meta[elementType.label ?? ""] = { op: "notEquals", value };
}
}
});
}
if (quotas.length) {
quotas.forEach(({ filterType, questionType }) => {
quotas.forEach(({ filterType, elementType }) => {
filters.quotas ??= {};
const quotaId = questionType.id;
const quotaId = elementType.id;
if (!quotaId) return;
const statusMap: Record<string, "screenedIn" | "screenedOut" | "screenedOutNotInQuota"> = {

View File

@@ -370,7 +370,7 @@ export const getResponseDownloadFile = async (
}
}
const { metaDataFields, questions, hiddenFields, variables, userAttributes } = extractSurveyDetails(
const { metaDataFields, elements, hiddenFields, variables, userAttributes } = extractSurveyDetails(
survey,
responses
);
@@ -399,7 +399,7 @@ export const getResponseDownloadFile = async (
"Notes",
"Tags",
...metaDataFields,
...questions.flat(),
...elements.flat(),
...variables,
...hiddenFields,
...userAttributes,
@@ -411,7 +411,7 @@ export const getResponseDownloadFile = async (
const jsonData = getResponsesJson(
survey,
responses,
questions,
elements,
userAttributes,
hiddenFields,
isQuotasAllowed
@@ -550,15 +550,15 @@ export const updateResponse = async (
};
const findAndDeleteUploadedFilesInResponse = async (response: TResponse, survey: TSurvey): Promise<void> => {
const questions = getElementsFromBlocks(survey.blocks);
const elements = getElementsFromBlocks(survey.blocks);
const fileUploadQuestions = new Set(
questions.filter((question) => question.type === TSurveyElementTypeEnum.FileUpload).map((q) => q.id)
const fileUploadElements = new Set(
elements.filter((element) => element.type === TSurveyElementTypeEnum.FileUpload).map((q) => q.id)
);
const fileUrls = Object.entries(response.data)
.filter(([questionId]) => fileUploadQuestions.has(questionId))
.flatMap(([, questionResponse]) => questionResponse as string[]);
.filter(([elementId]) => fileUploadElements.has(elementId))
.flatMap(([, elementResponse]) => elementResponse as string[]);
const deletionPromises = fileUrls.map(async (fileUrl) => {
try {

View File

@@ -26,33 +26,33 @@ import { getFormattedDateTimeString } from "../utils/datetime";
import { sanitizeString } from "../utils/strings";
/**
* Extracts choice IDs from response values for multiple choice questions
* Extracts choice IDs from response values for multiple choice elements
* @param responseValue - The response value (string for single choice, array for multi choice)
* @param question - The survey question containing choices
* @param element - The survey element containing choices
* @param language - The language to match against (defaults to "default")
* @returns Array of choice IDs
*/
export const extractChoiceIdsFromResponse = (
responseValue: TResponseDataValue,
question: TSurveyElement,
element: TSurveyElement,
language: string = "default"
): string[] => {
// Type guard to ensure the question has choices
if (
question.type !== "multipleChoiceMulti" &&
question.type !== "multipleChoiceSingle" &&
question.type !== "ranking" &&
question.type !== "pictureSelection"
element.type !== "multipleChoiceMulti" &&
element.type !== "multipleChoiceSingle" &&
element.type !== "ranking" &&
element.type !== "pictureSelection"
) {
return [];
}
const isPictureSelection = question.type === "pictureSelection";
const isPictureSelection = element.type === "pictureSelection";
if (!responseValue) {
return [];
}
// For picture selection questions, the response value is already choice ID(s)
// For picture selection elements, the response value is already choice ID(s)
if (isPictureSelection) {
if (Array.isArray(responseValue)) {
// Multi-selection: array of choice IDs
@@ -68,7 +68,7 @@ export const extractChoiceIdsFromResponse = (
// Helper function to find choice by label - eliminates duplication
const findChoiceByLabel = (choiceLabel: string): string | null => {
const targetChoice = question.choices.find((c) => {
const targetChoice = element.choices.find((c) => {
// Try exact language match first
if (c.label[defaultLanguage] === choiceLabel) {
return true;
@@ -93,13 +93,13 @@ export const extractChoiceIdsFromResponse = (
export const getChoiceIdByValue = (
value: string,
question: TSurveyMultipleChoiceElement | TSurveyRankingElement | TSurveyPictureSelectionElement
element: TSurveyMultipleChoiceElement | TSurveyRankingElement | TSurveyPictureSelectionElement
) => {
if (question.type === "pictureSelection") {
return question.choices.find((choice) => choice.imageUrl === value)?.id ?? "other";
if (element.type === "pictureSelection") {
return element.choices.find((choice) => choice.imageUrl === value)?.id ?? "other";
}
return question.choices.find((choice) => choice.label.default === value)?.id ?? "other";
return element.choices.find((choice) => choice.label.default === value)?.id ?? "other";
};
export const calculateTtcTotal = (ttc: TResponseTtc) => {
@@ -325,13 +325,12 @@ export const buildWhereClause = (survey: TSurvey, filterCriteria?: TResponseFilt
});
}
// For Questions Data
if (filterCriteria?.data) {
const data: Prisma.ResponseWhereInput[] = [];
Object.entries(filterCriteria.data).forEach(([key, val]) => {
const questions = getElementsFromBlocks(survey.blocks);
const question = questions.find((question) => question.id === key);
const elements = getElementsFromBlocks(survey.blocks);
const element = elements.find((element) => element.id === key);
switch (val.op) {
case "submitted":
@@ -365,7 +364,7 @@ export const buildWhereClause = (survey: TSurvey, filterCriteria?: TResponseFilt
equals: "",
},
},
// For address question
// For address element
{
data: {
path: [key],
@@ -444,29 +443,29 @@ export const buildWhereClause = (survey: TSurvey, filterCriteria?: TResponseFilt
});
break;
case "includesOne":
// * If the question includes an 'other' choice and the user has selected it:
// * - `predefinedLabels`: Collects labels from the question's choices that aren't selected by the user.
// * If the element includes an 'other' choice and the user has selected it:
// * - `predefinedLabels`: Collects labels from the element's choices that aren't selected by the user.
// * - `subsets`: Generates all possible non-empty permutations of subsets of these predefined labels.
// *
// * Depending on the question type (multiple or single choice), the filter is constructed:
// * Depending on the element type (multiple or single choice), the filter is constructed:
// * - For "multipleChoiceMulti": Filters out any combinations of choices that match the subsets of predefined labels.
// * - For "multipleChoiceSingle": Filters out any single predefined labels that match the user's selection.
const values: string[] = val.value.map((v) => v.toString());
const otherChoice =
question && (question.type === "multipleChoiceMulti" || question.type === "multipleChoiceSingle")
? question.choices.find((choice) => choice.id === "other")
element && (element.type === "multipleChoiceMulti" || element.type === "multipleChoiceSingle")
? element.choices.find((choice) => choice.id === "other")
: null;
if (
question &&
(question.type === "multipleChoiceMulti" || question.type === "multipleChoiceSingle") &&
question.choices.map((choice) => choice.id).includes("other") &&
element &&
(element.type === "multipleChoiceMulti" || element.type === "multipleChoiceSingle") &&
element.choices.map((choice) => choice.id).includes("other") &&
otherChoice &&
values.includes(otherChoice.label.default)
) {
const predefinedLabels: string[] = [];
question.choices.forEach((choice) => {
element.choices.forEach((choice) => {
Object.values(choice.label).forEach((label) => {
if (!values.includes(label)) {
predefinedLabels.push(label);
@@ -475,7 +474,7 @@ export const buildWhereClause = (survey: TSurvey, filterCriteria?: TResponseFilt
});
const subsets = generateAllPermutationsOfSubsets(predefinedLabels);
if (question.type === "multipleChoiceMulti") {
if (element.type === "multipleChoiceMulti") {
const subsetConditions = subsets.map((subset) => ({
data: { path: [key], equals: subset },
}));
@@ -665,18 +664,18 @@ export const extractSurveyDetails = (survey: TSurvey, responses: TResponse[]) =>
const metaDataFields = responses.length > 0 ? extracMetadataKeys(responses[0].meta) : [];
const modifiedSurvey = replaceHeadlineRecall(survey, "default");
const modifiedQuestions = getElementsFromBlocks(modifiedSurvey.blocks);
const modifiedElements = getElementsFromBlocks(modifiedSurvey.blocks);
const questions = modifiedQuestions.map((question, idx) => {
const headline = getTextContent(getLocalizedValue(question.headline, "default")) ?? question.id;
if (question.type === "matrix") {
return question.rows.map((row) => {
const elements = modifiedElements.map((element, idx) => {
const headline = getTextContent(getLocalizedValue(element.headline, "default")) ?? element.id;
if (element.type === "matrix") {
return element.rows.map((row) => {
return `${idx + 1}. ${headline} - ${getTextContent(getLocalizedValue(row.label, "default"))}`;
});
} else if (
question.type === "multipleChoiceMulti" ||
question.type === "multipleChoiceSingle" ||
question.type === "ranking"
element.type === "multipleChoiceMulti" ||
element.type === "multipleChoiceSingle" ||
element.type === "ranking"
) {
return [`${idx + 1}. ${headline}`, `${idx + 1}. ${headline} - Option ID`];
} else {
@@ -691,13 +690,13 @@ export const extractSurveyDetails = (survey: TSurvey, responses: TResponse[]) =>
: [];
const variables = survey.variables?.map((variable) => variable.name) || [];
return { metaDataFields, questions, hiddenFields, variables, userAttributes };
return { metaDataFields, elements, hiddenFields, variables, userAttributes };
};
export const getResponsesJson = (
survey: TSurvey,
responses: TResponseWithQuotas[],
questionsHeadlines: string[][],
elementsHeadlines: string[][],
userAttributes: string[],
hiddenFields: string[],
isQuotasAllowed: boolean = false
@@ -733,17 +732,17 @@ export const getResponsesJson = (
});
// survey response data
questionsHeadlines.forEach((questionHeadline) => {
const questionIndex = parseInt(questionHeadline[0]) - 1;
const questions = getElementsFromBlocks(survey.blocks);
const question = questions[questionIndex];
const answer = response.data[question.id];
elementsHeadlines.forEach((elementHeadline) => {
const elementIndex = parseInt(elementHeadline[0]) - 1;
const elements = getElementsFromBlocks(survey.blocks);
const element = elements[elementIndex];
const answer = response.data[element.id];
if (question.type === "matrix") {
// For matrix questions, we need to handle each row separately
questionHeadline.forEach((headline, index) => {
if (element.type === "matrix") {
// For matrix elements, we need to handle each row separately
elementHeadline.forEach((headline, index) => {
if (answer) {
const row = question.rows[index];
const row = element.rows[index];
if (row && row.label.default && answer[row.label.default] !== undefined) {
jsonData[idx][headline] = answer[row.label.default];
} else {
@@ -752,20 +751,20 @@ export const getResponsesJson = (
}
});
} else if (
question.type === "multipleChoiceMulti" ||
question.type === "multipleChoiceSingle" ||
question.type === "ranking"
element.type === "multipleChoiceMulti" ||
element.type === "multipleChoiceSingle" ||
element.type === "ranking"
) {
// Set the main response value
jsonData[idx][questionHeadline[0]] = processResponseData(answer);
jsonData[idx][elementHeadline[0]] = processResponseData(answer);
// Set the option IDs using the reusable function
if (questionHeadline[1]) {
const choiceIds = extractChoiceIdsFromResponse(answer, question, response.language || "default");
jsonData[idx][questionHeadline[1]] = choiceIds.join(", ");
if (elementHeadline[1]) {
const choiceIds = extractChoiceIdsFromResponse(answer, element, response.language || "default");
jsonData[idx][elementHeadline[1]] = choiceIds.join(", ");
}
} else {
jsonData[idx][questionHeadline[0]] = processResponseData(answer);
jsonData[idx][elementHeadline[0]] = processResponseData(answer);
}
});

View File

@@ -1,6 +1,6 @@
import { describe, expect, test, vi } from "vitest";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { convertResponseValue, getQuestionResponseMapping, processResponseData } from "./responses";
import { convertResponseValue, getElementResponseMapping, processResponseData } from "./responses";
// Mock the recall and i18n utils
vi.mock("@/lib/utils/recall", () => ({
@@ -295,7 +295,7 @@ describe("Response Processing", () => {
};
test("should map questions to responses correctly", () => {
const mapping = getQuestionResponseMapping(mockSurvey, mockResponse);
const mapping = getElementResponseMapping(mockSurvey, mockResponse);
expect(mapping).toHaveLength(2);
expect(mapping[0]).toEqual({
question: "Question 1",
@@ -334,7 +334,7 @@ describe("Response Processing", () => {
contactAttributes: {},
singleUseId: null,
};
const mapping = getQuestionResponseMapping(mockSurvey, response);
const mapping = getElementResponseMapping(mockSurvey, response);
expect(mapping).toHaveLength(2);
expect(mapping[0].response).toBe("");
expect(mapping[1].response).toBe("");
@@ -412,8 +412,8 @@ describe("Response Processing", () => {
contactAttributes: {},
singleUseId: null,
};
const mapping = getQuestionResponseMapping(survey, response);
expect(mapping[0].question).toBe("Question 1 EN");
const mapping = getElementResponseMapping(survey, response);
expect(mapping[0].element).toBe("Question 1 EN");
});
test("should handle null response language", () => {
@@ -441,9 +441,9 @@ describe("Response Processing", () => {
contactAttributes: {},
singleUseId: null,
};
const mapping = getQuestionResponseMapping(mockSurvey, response);
const mapping = getElementResponseMapping(mockSurvey, response);
expect(mapping).toHaveLength(2);
expect(mapping[0].question).toBe("Question 1");
expect(mapping[0].element).toBe("Question 1");
});
test("should handle undefined response language", () => {
@@ -471,9 +471,9 @@ describe("Response Processing", () => {
contactAttributes: {},
singleUseId: null,
};
const mapping = getQuestionResponseMapping(mockSurvey, response);
const mapping = getElementResponseMapping(mockSurvey, response);
expect(mapping).toHaveLength(2);
expect(mapping[0].question).toBe("Question 1");
expect(mapping[0].element).toBe("Question 1");
});
test("should handle empty survey languages", () => {
@@ -505,9 +505,9 @@ describe("Response Processing", () => {
contactAttributes: {},
singleUseId: null,
};
const mapping = getQuestionResponseMapping(survey, response);
const mapping = getElementResponseMapping(survey, response);
expect(mapping).toHaveLength(2);
expect(mapping[0].question).toBe("Question 1"); // Should fallback to default
expect(mapping[0].element).toBe("Question 1"); // Should fallback to default
});
});
});

View File

@@ -9,9 +9,9 @@ import { getLanguageCode, getLocalizedValue } from "./i18n/utils";
// function to convert response value of type string | number | string[] or Record<string, string> to string | string[]
export const convertResponseValue = (
answer: TResponseDataValue,
question: TSurveyElement
element: TSurveyElement
): string | string[] => {
switch (question.type) {
switch (element.type) {
case "ranking":
case "fileUpload":
if (typeof answer === "string") {
@@ -20,11 +20,11 @@ export const convertResponseValue = (
case "pictureSelection":
if (typeof answer === "string") {
const imageUrl = question.choices.find((choice) => choice.id === answer)?.imageUrl;
const imageUrl = element.choices.find((choice) => choice.id === answer)?.imageUrl;
return imageUrl ? [imageUrl] : [];
} else if (Array.isArray(answer)) {
return answer
.map((answerId) => question.choices.find((choice) => choice.id === answerId)?.imageUrl)
.map((answerId) => element.choices.find((choice) => choice.id === answerId)?.imageUrl)
.filter((url): url is string => url !== undefined);
} else return [];
@@ -33,35 +33,32 @@ export const convertResponseValue = (
}
};
export const getQuestionResponseMapping = (
export const getElementResponseMapping = (
survey: TSurvey,
response: TResponse
): { question: string; response: string | string[]; type: TSurveyElementTypeEnum }[] => {
const questionResponseMapping: {
question: string;
): { element: string; response: string | string[]; type: TSurveyElementTypeEnum }[] => {
const elementResponseMapping: {
element: string;
response: string | string[];
type: TSurveyElementTypeEnum;
}[] = [];
const responseLanguageCode = getLanguageCode(survey.languages, response.language);
const questions = getElementsFromBlocks(survey.blocks);
const elements = getElementsFromBlocks(survey.blocks);
for (const question of questions) {
const answer = response.data[question.id];
for (const element of elements) {
const answer = response.data[element.id];
questionResponseMapping.push({
question: getTextContent(
parseRecallInfo(
getLocalizedValue(question.headline, responseLanguageCode ?? "default"),
response.data
)
elementResponseMapping.push({
element: getTextContent(
parseRecallInfo(getLocalizedValue(element.headline, responseLanguageCode ?? "default"), response.data)
),
response: convertResponseValue(answer, question),
type: question.type,
response: convertResponseValue(answer, element),
type: element.type,
});
}
return questionResponseMapping;
return elementResponseMapping;
};
export const processResponseData = (responseData: TResponseDataValue): string => {

View File

@@ -69,20 +69,20 @@ export const checkForInvalidImagesInQuestions = (questions: TSurveyQuestion[]) =
* Validates a single choice's image URL
* @param choice - Choice to validate
* @param choiceIdx - Index of the choice for error reporting
* @param questionIdx - Index of the question for error reporting
* @param elementIdx - Index of the element for error reporting
* @param blockName - Block name for error reporting
* @returns Result with void data on success or Error on failure
*/
const validateChoiceImage = (
choice: TSurveyPictureChoice,
choiceIdx: number,
questionIdx: number,
elementIdx: number,
blockName: string
): Result<void, Error> => {
if (choice.imageUrl && !isValidImageFile(choice.imageUrl)) {
return err(
new Error(
`Invalid image URL in choice ${choiceIdx + 1} of question ${questionIdx + 1} of block "${blockName}"`
`Invalid image URL in choice ${choiceIdx + 1} of question ${elementIdx + 1} of block "${blockName}"`
)
);
}
@@ -91,18 +91,18 @@ const validateChoiceImage = (
/**
* Validates choice images for picture selection elements
* Only picture selection questions have imageUrl in choices
* Only picture selection elements have imageUrl in choices
* @param element - Element with choices to validate
* @param questionIdx - Index of the question for error reporting
* @param elementIdx - Index of the element for error reporting
* @param blockName - Block name for error reporting
* @returns Result with void data on success or Error on failure
*/
const validatePictureSelectionChoiceImages = (
element: TSurveyElement,
questionIdx: number,
elementIdx: number,
blockName: string
): Result<void, Error> => {
// Only validate choices for picture selection questions
// Only validate choices for picture selection elements
if (element.type !== TSurveyElementTypeEnum.PictureSelection) {
return ok(undefined);
}
@@ -112,7 +112,7 @@ const validatePictureSelectionChoiceImages = (
}
for (let choiceIdx = 0; choiceIdx < element.choices.length; choiceIdx++) {
const result = validateChoiceImage(element.choices[choiceIdx], choiceIdx, questionIdx, blockName);
const result = validateChoiceImage(element.choices[choiceIdx], choiceIdx, elementIdx, blockName);
if (!result.ok) {
return result;
}

View File

@@ -259,7 +259,7 @@ describe("surveyLogic", () => {
];
const result = performActions(mockSurvey, actions, data, initialVars);
expect(result.calculations.v).toBe(3);
expect(result.requiredQuestionIds).toContain("q2");
expect(result.requiredElementIds).toContain("q2");
expect(result.jumpTarget).toBe("q3");
});

View File

@@ -268,12 +268,12 @@ const evaluateSingleCondition = (
? getRightOperandValue(localSurvey, data, variablesData, condition.rightOperand)
: undefined;
const questions = getElementsFromBlocks(localSurvey.blocks);
const elements = getElementsFromBlocks(localSurvey.blocks);
let leftField: TSurveyElement | TSurveyVariable | string;
if (condition.leftOperand?.type === "element") {
leftField = questions.find((q) => q.id === condition.leftOperand?.value) ?? "";
leftField = elements.find((q) => q.id === condition.leftOperand?.value) ?? "";
} else if (condition.leftOperand?.type === "variable") {
leftField = localSurvey.variables.find((v) => v.id === condition.leftOperand?.value) as TSurveyVariable;
} else if (condition.leftOperand?.type === "hiddenField") {
@@ -285,7 +285,7 @@ const evaluateSingleCondition = (
let rightField: TSurveyElement | TSurveyVariable | string;
if (condition.rightOperand?.type === "element") {
rightField = questions.find((q) => q.id === condition.rightOperand?.value) ?? "";
rightField = elements.find((q) => q.id === condition.rightOperand?.value) ?? "";
} else if (condition.rightOperand?.type === "variable") {
rightField = localSurvey.variables.find(
(v) => v.id === condition.rightOperand?.value
@@ -312,7 +312,7 @@ const evaluateSingleCondition = (
typeof leftValue === "string" &&
typeof rightValue === "string"
) {
// when left value is of date question and right value is string
// when left value is of date element and right value is string
return new Date(leftValue).getTime() === new Date(rightValue).getTime();
}
}
@@ -340,7 +340,7 @@ const evaluateSingleCondition = (
leftValue === rightValue
);
case "doesNotEqual":
// when left value is of picture selection question and right value is its option
// when left value is of picture selection element and right value is its option
if (
condition.leftOperand.type === "element" &&
(leftField as TSurveyElement).type === TSurveyElementTypeEnum.PictureSelection &&
@@ -351,7 +351,7 @@ const evaluateSingleCondition = (
return !leftValue.includes(rightValue);
}
// when left value is of date question and right value is string
// when left value is of date element and right value is string
if (
condition.leftOperand.type === "element" &&
(leftField as TSurveyElement).type === TSurveyElementTypeEnum.Date &&
@@ -512,13 +512,13 @@ const getLeftOperandValue = (
) => {
switch (leftOperand.type) {
case "element":
const questions = getElementsFromBlocks(localSurvey.blocks);
const currentQuestion = questions.find((q) => q.id === leftOperand.value);
if (!currentQuestion) return undefined;
const elements = getElementsFromBlocks(localSurvey.blocks);
const currentElement = elements.find((q) => q.id === leftOperand.value);
if (!currentElement) return undefined;
const responseValue = data[leftOperand.value];
if (currentQuestion.type === "openText" && currentQuestion.inputType === "number") {
if (currentElement.type === "openText" && currentElement.inputType === "number") {
if (responseValue === undefined) return undefined;
if (typeof responseValue === "string" && responseValue.trim() === "") return undefined;
@@ -526,11 +526,11 @@ const getLeftOperandValue = (
return isNaN(numberValue) ? undefined : numberValue;
}
if (currentQuestion.type === "multipleChoiceSingle" || currentQuestion.type === "multipleChoiceMulti") {
const isOthersEnabled = currentQuestion.choices.at(-1)?.id === "other";
if (currentElement.type === "multipleChoiceSingle" || currentElement.type === "multipleChoiceMulti") {
const isOthersEnabled = currentElement.choices.at(-1)?.id === "other";
if (typeof responseValue === "string") {
const choice = currentQuestion.choices.find((choice) => {
const choice = currentElement.choices.find((choice) => {
return getLocalizedValue(choice.label, selectedLanguage) === responseValue;
});
@@ -546,7 +546,7 @@ const getLeftOperandValue = (
} else if (Array.isArray(responseValue)) {
let choices: string[] = [];
responseValue.forEach((value) => {
const foundChoice = currentQuestion.choices.find((choice) => {
const foundChoice = currentElement.choices.find((choice) => {
return getLocalizedValue(choice.label, selectedLanguage) === value;
});
@@ -563,23 +563,23 @@ const getLeftOperandValue = (
}
if (
currentQuestion.type === "matrix" &&
currentElement.type === "matrix" &&
typeof responseValue === "object" &&
!Array.isArray(responseValue)
) {
if (leftOperand.meta && leftOperand.meta.row !== undefined) {
const rowIndex = Number(leftOperand.meta.row);
if (isNaN(rowIndex) || rowIndex < 0 || rowIndex >= currentQuestion.rows.length) {
if (isNaN(rowIndex) || rowIndex < 0 || rowIndex >= currentElement.rows.length) {
return undefined;
}
const row = getLocalizedValue(currentQuestion.rows[rowIndex].label, selectedLanguage);
const row = getLocalizedValue(currentElement.rows[rowIndex].label, selectedLanguage);
const rowValue = responseValue[row];
if (rowValue === "") return "";
if (rowValue) {
const columnIndex = currentQuestion.columns.findIndex((column) => {
const columnIndex = currentElement.columns.findIndex((column) => {
return getLocalizedValue(column.label, selectedLanguage) === rowValue;
});
if (columnIndex === -1) return undefined;
@@ -630,11 +630,11 @@ export const performActions = (
calculationResults: TResponseVariables
): {
jumpTarget: string | undefined;
requiredQuestionIds: string[];
requiredElementIds: string[];
calculations: TResponseVariables;
} => {
let jumpTarget: string | undefined;
const requiredQuestionIds: string[] = [];
const requiredElementIds: string[] = [];
const calculations: TResponseVariables = { ...calculationResults };
actions.forEach((action) => {
@@ -644,7 +644,7 @@ export const performActions = (
if (result !== undefined) calculations[action.variableId] = result;
break;
case "requireAnswer":
requiredQuestionIds.push(action.target);
requiredElementIds.push(action.target);
break;
case "jumpToBlock":
if (!jumpTarget) {
@@ -654,7 +654,7 @@ export const performActions = (
}
});
return { jumpTarget, requiredQuestionIds, calculations };
return { jumpTarget, requiredElementIds, calculations };
};
const performCalculation = (

View File

@@ -9,7 +9,7 @@ import { getLocalizedValue } from "@/lib/i18n/utils";
import { parseRecallInfo } from "@/lib/utils/recall";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
interface QuestionSkipProps {
interface ElementSkipProps {
skippedQuestions: string[] | undefined;
status: string;
questions: TSurveyElement[];
@@ -17,13 +17,13 @@ interface QuestionSkipProps {
responseData: TResponseData;
}
export const QuestionSkip = ({
export const ElementSkip = ({
skippedQuestions,
status,
questions,
isFirstQuestionAnswered,
responseData,
}: QuestionSkipProps) => {
}: ElementSkipProps) => {
const { t } = useTranslation();
return (
<div>

View File

@@ -18,7 +18,7 @@ import { ResponseBadges } from "@/modules/ui/components/response-badges";
interface RenderResponseProps {
responseData: TResponseDataValue;
question: TSurveyElement;
element: TSurveyElement;
survey: TSurvey;
language: string | null;
isExpanded?: boolean;
@@ -27,7 +27,7 @@ interface RenderResponseProps {
export const RenderResponse: React.FC<RenderResponseProps> = ({
responseData,
question,
element,
survey,
language,
isExpanded = true,
@@ -48,16 +48,15 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
return String(data);
}
};
const questionType = question.type;
switch (questionType) {
switch (element.type) {
case TSurveyElementTypeEnum.Rating:
if (typeof responseData === "number") {
return (
<RatingResponse
scale={question.scale}
scale={element.scale}
answer={responseData}
range={question.range}
addColors={question.isColorCodingEnabled}
range={element.range}
addColors={element.isColorCodingEnabled}
/>
);
}
@@ -75,7 +74,7 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
if (Array.isArray(responseData)) {
return (
<PictureSelectionResponse
choices={question.choices}
choices={element.choices}
selected={responseData}
isExpanded={isExpanded}
showId={showId}
@@ -92,7 +91,7 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
if (typeof responseData === "object" && !Array.isArray(responseData)) {
return (
<>
{question.rows.map((row) => {
{element.rows.map((row) => {
const languagCode = getLanguageCode(survey.languages, language);
const rowValueInSelectedLanguage = getLocalizedValue(row.label, languagCode);
if (!responseData[rowValueInSelectedLanguage]) return null;
@@ -153,7 +152,7 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
case TSurveyElementTypeEnum.MultipleChoiceSingle:
case TSurveyElementTypeEnum.Ranking:
if (typeof responseData === "string" || typeof responseData === "number") {
const choiceId = getChoiceIdByValue(responseData.toString(), question);
const choiceId = getChoiceIdByValue(responseData.toString(), element);
return (
<ResponseBadges
items={[{ value: responseData.toString(), id: choiceId }]}
@@ -163,12 +162,12 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
);
} else if (Array.isArray(responseData)) {
const itemsArray = responseData.map((choice) => {
const choiceId = getChoiceIdByValue(choice, question);
const choiceId = getChoiceIdByValue(choice, element);
return { value: choice, id: choiceId };
});
return (
<>
{questionType === TSurveyElementTypeEnum.Ranking ? (
{element.type === TSurveyElementTypeEnum.Ranking ? (
<RankingResponse value={itemsArray} isExpanded={isExpanded} showId={showId} />
) : (
<ResponseBadges items={itemsArray} isExpanded={isExpanded} showId={showId} />

View File

@@ -10,8 +10,8 @@ import { parseRecallInfo } from "@/lib/utils/recall";
import { ResponseCardQuotas } from "@/modules/ee/quotas/components/single-response-card-quotas";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { isValidValue } from "../util";
import { ElementSkip } from "./ElementSkip";
import { HiddenFields } from "./HiddenFields";
import { QuestionSkip } from "./QuestionSkip";
import { RenderResponse } from "./RenderResponse";
import { ResponseVariables } from "./ResponseVariables";
import { VerifiedEmail } from "./VerifiedEmail";
@@ -54,7 +54,7 @@ export const SingleResponseCardBody = ({
return (
<div className="p-6">
{survey.welcomeCard.enabled && (
<QuestionSkip
<ElementSkip
skippedQuestions={[]}
questions={questions}
status={"welcomeCard"}
@@ -94,7 +94,7 @@ export const SingleResponseCardBody = ({
</p>
<div dir="auto">
<RenderResponse
question={question}
element={question}
survey={survey}
responseData={response.data[question.id]}
language={response.language}
@@ -103,7 +103,7 @@ export const SingleResponseCardBody = ({
</div>
</div>
) : (
<QuestionSkip
<ElementSkip
skippedQuestions={skipped}
questions={questions}
responseData={response.data}

View File

@@ -5,7 +5,7 @@ import {
TSurveyQuestionTypeEnum,
} from "@formbricks/types/surveys/types";
import { MAX_OTHER_OPTION_LENGTH } from "@/lib/constants";
import { validateOtherOptionLength, validateOtherOptionLengthForMultipleChoice } from "../question";
import { validateOtherOptionLength, validateOtherOptionLengthForMultipleChoice } from "../element";
vi.mock("@/lib/i18n/utils", () => ({
getLocalizedValue: vi.fn().mockImplementation((value, language) => {

View File

@@ -1,7 +1,7 @@
import { z } from "zod";
import { sendToPipeline } from "@/app/lib/pipelines";
import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client";
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/question";
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
import { responses } from "@/modules/api/v2/lib/response";
import { handleApiError } from "@/modules/api/v2/lib/utils";
import { getEnvironmentId } from "@/modules/api/v2/management/lib/helper";

View File

@@ -2,7 +2,7 @@ import { Response } from "@prisma/client";
import { NextRequest } from "next/server";
import { sendToPipeline } from "@/app/lib/pipelines";
import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client";
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/question";
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
import { responses } from "@/modules/api/v2/lib/response";
import { handleApiError } from "@/modules/api/v2/lib/utils";
import { getEnvironmentId } from "@/modules/api/v2/management/lib/helper";

View File

@@ -19,14 +19,14 @@ interface LocalizedEditorProps {
value: TI18nString | undefined;
localSurvey: TSurvey;
isInvalid: boolean;
updateQuestion: any;
updateElement: any;
selectedLanguageCode: string;
setSelectedLanguageCode: (languageCode: string) => void;
questionIdx: number;
elementIdx: number;
firstRender: boolean;
setFirstRender?: Dispatch<SetStateAction<boolean>>;
locale: TUserLocale;
questionId: string;
elementId: string;
isCard?: boolean; // Flag to indicate if this is a welcome/ending card
autoFocus?: boolean;
isExternalUrlsAllowed?: boolean;
@@ -52,21 +52,21 @@ export function LocalizedEditor({
value,
localSurvey,
isInvalid,
updateQuestion,
updateElement,
selectedLanguageCode,
setSelectedLanguageCode,
questionIdx,
elementIdx,
firstRender,
setFirstRender,
locale,
questionId,
elementId,
isCard,
autoFocus,
isExternalUrlsAllowed,
suppressUpdates,
}: Readonly<LocalizedEditorProps>) {
// Derive questions from blocks for migrated surveys
const questions = useMemo(() => getElementsFromBlocks(localSurvey.blocks), [localSurvey.blocks]);
// Derive elements from blocks for migrated surveys
const elements = useMemo(() => getElementsFromBlocks(localSurvey.blocks), [localSurvey.blocks]);
const { t } = useTranslation();
const isInComplete = useMemo(
@@ -95,7 +95,7 @@ export function LocalizedEditor({
return html;
}}
key={`${questionId}-${id}-${selectedLanguageCode}`}
key={`${elementId}-${id}-${selectedLanguageCode}`}
setFirstRender={setFirstRender}
setText={(v: string) => {
// Early exit if updates are suppressed (e.g., during deletion)
@@ -109,17 +109,17 @@ export function LocalizedEditor({
sanitizedContent = v.replaceAll(/<a[^>]*>(.*?)<\/a>/gi, "$1");
}
// Check if the question still exists before updating
const currentQuestion = questions[questionIdx];
// Check if the elements still exists before updating
const currentElement = elements[elementIdx];
// if this is a card, we wanna check if the card exists in the localSurvey
if (isCard) {
const isWelcomeCard = questionIdx === -1;
const isEndingCard = questionIdx >= questions.length;
const isWelcomeCard = elementIdx === -1;
const isEndingCard = elementIdx >= elements.length;
// For ending cards, check if the field exists before updating
if (isEndingCard) {
const ending = localSurvey.endings.find((ending) => ending.id === questionId);
const ending = localSurvey.endings.find((ending) => ending.id === elementId);
// If the field doesn't exist on the ending card, don't create it
if (!ending || ending[id] === undefined) {
return;
@@ -135,21 +135,21 @@ export function LocalizedEditor({
...value,
[selectedLanguageCode]: sanitizedContent,
};
updateQuestion({ [id]: translatedContent });
updateElement({ [id]: translatedContent });
return;
}
// Check if the field exists on the question (not just if it's not undefined)
if (currentQuestion && id in currentQuestion && currentQuestion[id] !== undefined) {
// Check if the field exists on the element (not just if it's not undefined)
if (currentElement && id in currentElement && currentElement[id] !== undefined) {
const translatedContent = {
...value,
[selectedLanguageCode]: sanitizedContent,
};
updateQuestion(questionIdx, { [id]: translatedContent });
updateElement(elementIdx, { [id]: translatedContent });
}
}}
localSurvey={localSurvey}
questionId={questionId}
elementId={elementId}
selectedLanguageCode={selectedLanguageCode}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>

View File

@@ -8,7 +8,7 @@ import Link from "next/link";
import type { FC } from "react";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import type { TSurvey, TSurveyLanguage, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import type { TSurvey, TSurveyLanguage } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { cn } from "@/lib/cn";
import { addMultiLanguageLabels, extractLanguageCodes } from "@/lib/i18n/utils";
@@ -25,8 +25,8 @@ interface MultiLanguageCardProps {
localSurvey: TSurvey;
projectLanguages: Language[];
setLocalSurvey: (survey: TSurvey) => void;
activeQuestionId: TSurveyQuestionId | null;
setActiveQuestionId: (questionId: TSurveyQuestionId | null) => void;
activeElementId: string | null;
setActiveElementId: (elementId: string | null) => void;
isMultiLanguageAllowed?: boolean;
isFormbricksCloud: boolean;
setSelectedLanguageCode: (language: string) => void;
@@ -43,9 +43,9 @@ export interface ConfirmationModalProps {
}
export const MultiLanguageCard: FC<MultiLanguageCardProps> = ({
activeQuestionId,
activeElementId,
localSurvey,
setActiveQuestionId,
setActiveElementId,
setLocalSurvey,
projectLanguages,
isMultiLanguageAllowed,
@@ -55,7 +55,7 @@ export const MultiLanguageCard: FC<MultiLanguageCardProps> = ({
}) => {
const { t } = useTranslation();
const environmentId = localSurvey.environmentId;
const open = activeQuestionId === "multiLanguage";
const open = activeElementId === "multiLanguage";
const [isMultiLanguageActivated, setIsMultiLanguageActivated] = useState(localSurvey.languages.length > 1);
const [confirmationModalInfo, setConfirmationModalInfo] = useState<ConfirmationModalProps>({
title: "",
@@ -72,9 +72,9 @@ export const MultiLanguageCard: FC<MultiLanguageCardProps> = ({
const setOpen = (open: boolean) => {
if (open) {
setActiveQuestionId("multiLanguage");
setActiveElementId("multiLanguage");
} else {
setActiveQuestionId(null);
setActiveElementId(null);
}
};
@@ -279,7 +279,7 @@ export const MultiLanguageCard: FC<MultiLanguageCardProps> = ({
defaultLanguage={defaultLanguage}
localSurvey={localSurvey}
projectLanguages={projectLanguages}
setActiveQuestionId={setActiveQuestionId}
setActiveElementId={setActiveElementId}
setSelectedLanguageCode={setSelectedLanguageCode}
updateSurveyLanguages={updateSurveyLanguages}
locale={locale}

View File

@@ -11,7 +11,7 @@ interface SecondaryLanguageSelectProps {
projectLanguages: Language[];
defaultLanguage: Language;
setSelectedLanguageCode: (languageCode: string) => void;
setActiveQuestionId: (questionId: string) => void;
setActiveElementId: (elementId: string) => void;
localSurvey: TSurvey;
updateSurveyLanguages: (language: Language) => void;
locale: TUserLocale;
@@ -21,7 +21,7 @@ export function SecondaryLanguageSelect({
projectLanguages,
defaultLanguage,
setSelectedLanguageCode,
setActiveQuestionId,
setActiveElementId,
localSurvey,
updateSurveyLanguages,
locale,
@@ -33,7 +33,7 @@ export function SecondaryLanguageSelect({
);
};
const questions = getElementsFromBlocks(localSurvey.blocks);
const elements = getElementsFromBlocks(localSurvey.blocks);
return (
<div className="space-y-2">
@@ -49,7 +49,7 @@ export function SecondaryLanguageSelect({
language={language}
onEdit={() => {
setSelectedLanguageCode(language.code);
setActiveQuestionId(questions[0]?.id);
setActiveElementId(elements[0]?.id);
}}
onToggle={() => {
updateSurveyLanguages(language);

View File

@@ -24,7 +24,7 @@ import { isLight, mixColor } from "@/lib/utils/colors";
import { parseRecallInfo } from "@/lib/utils/recall";
import { RatingSmiley } from "@/modules/analysis/components/RatingSmiley";
import { getNPSOptionColor, getRatingNumberOptionColor } from "../lib/utils";
import { QuestionHeader } from "./email-question-header";
import { QuestionHeader } from "./email-element-header";
interface PreviewEmailTemplateProps {
survey: TSurvey;

View File

@@ -3,7 +3,7 @@ import { FileDigitIcon, FileType2Icon } from "lucide-react";
import type { TOrganization } from "@formbricks/types/organizations";
import type { TResponse } from "@formbricks/types/responses";
import { type TSurvey } from "@formbricks/types/surveys/types";
import { getQuestionResponseMapping } from "@/lib/responses";
import { getElementResponseMapping } from "@/lib/responses";
import { getTranslate } from "@/lingodotdev/server";
import { renderEmailResponseValue } from "@/modules/email/emails/lib/utils";
import { EmailButton } from "../../components/email-button";
@@ -26,7 +26,7 @@ export async function ResponseFinishedEmail({
environmentId,
organization,
}: ResponseFinishedEmailProps): Promise<React.JSX.Element> {
const questions = getQuestionResponseMapping(survey, response);
const questions = getElementResponseMapping(survey, response);
const t = await getTranslate();
return (
@@ -44,9 +44,9 @@ export async function ResponseFinishedEmail({
{questions.map((question) => {
if (!question.response) return;
return (
<Row key={question.question}>
<Row key={question.element}>
<Column className="w-full font-medium">
<Text className="mb-2 text-sm">{question.question}</Text>
<Text className="mb-2 text-sm">{question.element}</Text>
{renderEmailResponseValue(question.response, question.type, t)}
</Column>
</Row>

View File

@@ -27,7 +27,7 @@ import {
} from "@/modules/ui/components/dropdown-menu";
import { Input } from "@/modules/ui/components/input";
const questionIconMapping = {
const elementIconMapping = {
openText: MessageSquareTextIcon,
multipleChoiceSingle: Rows3Icon,
multipleChoiceMulti: ListIcon,
@@ -42,8 +42,8 @@ const questionIconMapping = {
interface RecallItemSelectProps {
localSurvey: TSurvey;
questionId: TSurveyElementId;
addRecallItem: (question: TSurveyRecallItem) => void;
elementId: TSurveyElementId;
addRecallItem: (item: TSurveyRecallItem) => void;
setShowRecallItemSelect: (show: boolean) => void;
recallItems: TSurveyRecallItem[];
selectedLanguageCode: string;
@@ -52,7 +52,7 @@ interface RecallItemSelectProps {
export const RecallItemSelect = ({
localSurvey,
questionId,
elementId,
addRecallItem,
setShowRecallItemSelect,
recallItems,
@@ -60,18 +60,18 @@ export const RecallItemSelect = ({
}: RecallItemSelectProps) => {
const [searchValue, setSearchValue] = useState("");
const { t } = useTranslation();
const isNotAllowedQuestionType = (question: TSurveyElement): boolean => {
const isNotAllowedElementType = (element: TSurveyElement): boolean => {
return (
question.type === TSurveyElementTypeEnum.FileUpload ||
question.type === TSurveyElementTypeEnum.CTA ||
question.type === TSurveyElementTypeEnum.Consent ||
question.type === TSurveyElementTypeEnum.PictureSelection ||
question.type === TSurveyElementTypeEnum.Cal ||
question.type === TSurveyElementTypeEnum.Matrix
element.type === TSurveyElementTypeEnum.FileUpload ||
element.type === TSurveyElementTypeEnum.CTA ||
element.type === TSurveyElementTypeEnum.Consent ||
element.type === TSurveyElementTypeEnum.PictureSelection ||
element.type === TSurveyElementTypeEnum.Cal ||
element.type === TSurveyElementTypeEnum.Matrix
);
};
const questions = useMemo(() => getElementsFromBlocks(localSurvey.blocks), [localSurvey.blocks]);
const elements = useMemo(() => getElementsFromBlocks(localSurvey.blocks), [localSurvey.blocks]);
const recallItemIds = useMemo(() => {
return recallItems.map((recallItem) => recallItem.id);
@@ -108,30 +108,28 @@ export const RecallItemSelect = ({
return [];
}, [localSurvey.variables, recallItemIds]);
const surveyQuestionRecallItems = useMemo(() => {
const isWelcomeCard = questionId === "start";
const surveyElementRecallItems = useMemo(() => {
const isWelcomeCard = elementId === "start";
if (isWelcomeCard) return [];
const isEndingCard = !questions.map((question) => question.id).includes(questionId);
const isEndingCard = !elements.map((element) => element.id).includes(elementId);
const idx = isEndingCard
? questions.length
: questions.findIndex((recallQuestion) => recallQuestion.id === questionId);
const filteredQuestions = questions
.filter((question, index) => {
const notAllowed = isNotAllowedQuestionType(question);
return (
!recallItemIds.includes(question.id) && !notAllowed && question.id !== questionId && idx > index
);
? elements.length
: elements.findIndex((recallElement) => recallElement.id === elementId);
const filteredElements = elements
.filter((element, index) => {
const notAllowed = isNotAllowedElementType(element);
return !recallItemIds.includes(element.id) && !notAllowed && element.id !== elementId && idx > index;
})
.map((question) => {
return { id: question.id, label: question.headline[selectedLanguageCode], type: "element" as const };
.map((element) => {
return { id: element.id, label: element.headline[selectedLanguageCode], type: "element" as const };
});
return filteredQuestions;
}, [questionId, questions, recallItemIds, selectedLanguageCode]);
return filteredElements;
}, [elementId, elements, recallItemIds, selectedLanguageCode]);
const filteredRecallItems: TSurveyRecallItem[] = useMemo(() => {
return [...surveyQuestionRecallItems, ...hiddenFieldRecallItems, ...variableRecallItems].filter(
return [...surveyElementRecallItems, ...hiddenFieldRecallItems, ...variableRecallItems].filter(
(recallItems) => {
if (searchValue.trim() === "") return true;
else {
@@ -139,14 +137,14 @@ export const RecallItemSelect = ({
}
}
);
}, [surveyQuestionRecallItems, hiddenFieldRecallItems, variableRecallItems, searchValue]);
}, [surveyElementRecallItems, hiddenFieldRecallItems, variableRecallItems, searchValue]);
const getRecallItemIcon = (recallItem: TSurveyRecallItem) => {
switch (recallItem.type) {
case "element":
const question = questions.find((question) => question.id === recallItem.id);
if (question) {
return questionIconMapping[question?.type as keyof typeof questionIconMapping];
const element = elements.find((element) => element.id === recallItem.id);
if (element) {
return elementIconMapping[element?.type as keyof typeof elementIconMapping];
}
case "hiddenField":
return EyeOffIcon;

View File

@@ -16,8 +16,8 @@ import {
recallToHeadline,
replaceRecallInfoWithUnderline,
} from "@/lib/utils/recall";
import { FallbackInput } from "@/modules/survey/components/question-form-input/components/fallback-input";
import { RecallItemSelect } from "@/modules/survey/components/question-form-input/components/recall-item-select";
import { FallbackInput } from "@/modules/survey/components/element-form-input/components/fallback-input";
import { RecallItemSelect } from "@/modules/survey/components/element-form-input/components/recall-item-select";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { Button } from "@/modules/ui/components/button";
@@ -33,7 +33,7 @@ interface RecallWrapperProps {
value: string | undefined;
onChange: (val: string, recallItems: TSurveyRecallItem[], fallbacks: { [id: string]: string }) => void;
localSurvey: TSurvey;
questionId: string;
elementId: string;
render: (props: RecallWrapperRenderProps) => React.ReactNode;
usedLanguageCode: string;
isRecallAllowed: boolean;
@@ -44,7 +44,7 @@ export const RecallWrapper = ({
value,
onChange,
localSurvey,
questionId,
elementId,
render,
usedLanguageCode,
isRecallAllowed,
@@ -190,9 +190,9 @@ export const RecallWrapper = ({
const info = extractRecallInfo(recallItem.label);
if (info) {
const recallItemId = extractId(info);
const questions = getElementsFromBlocks(localSurvey.blocks);
const recallQuestion = questions.find((q) => q.id === recallItemId);
if (recallQuestion) {
const elements = getElementsFromBlocks(localSurvey.blocks);
const recallElement = elements.find((e) => e.id === recallItemId);
if (recallElement) {
// replace nested recall with "___"
return [recallItem.label.replace(info, "___")];
}
@@ -257,7 +257,7 @@ export const RecallWrapper = ({
{showRecallItemSelect && (
<RecallItemSelect
localSurvey={localSurvey}
questionId={questionId}
elementId={elementId}
addRecallItem={addRecallItem}
setShowRecallItemSelect={setShowRecallItemSelect}
recallItems={recallItems}

View File

@@ -17,8 +17,8 @@ import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { useSyncScroll } from "@/lib/utils/hooks/useSyncScroll";
import { recallToHeadline } from "@/lib/utils/recall";
import { LocalizedEditor } from "@/modules/ee/multi-language-surveys/components/localized-editor";
import { MultiLangWrapper } from "@/modules/survey/components/question-form-input/components/multi-lang-wrapper";
import { RecallWrapper } from "@/modules/survey/components/question-form-input/components/recall-wrapper";
import { MultiLangWrapper } from "@/modules/survey/components/element-form-input/components/multi-lang-wrapper";
import { RecallWrapper } from "@/modules/survey/components/element-form-input/components/recall-wrapper";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { Button } from "@/modules/ui/components/button";
import { FileInput } from "@/modules/ui/components/file-input";
@@ -37,12 +37,12 @@ import {
isValueIncomplete,
} from "./utils";
interface QuestionFormInputProps {
interface ElementFormInputProps {
id: string;
value: TI18nString | undefined;
localSurvey: TSurvey;
questionIdx: number;
updateQuestion?: (questionIdx: number, data: Partial<TSurveyElement>) => void;
elementIdx: number;
updateElement?: (elementIdx: number, data: Partial<TSurveyElement>) => void;
updateSurvey?: (data: Partial<TSurveyEndScreenCard> | Partial<TSurveyRedirectUrlCard>) => void;
updateChoice?: (choiceIdx: number, data: Partial<TSurveyElementChoice>) => void;
updateMatrixLabel?: (index: number, type: "row" | "column", matrixLabel: TI18nString) => void;
@@ -63,12 +63,12 @@ interface QuestionFormInputProps {
isExternalUrlsAllowed?: boolean;
}
export const QuestionFormInput = ({
export const ElementFormInput = ({
id,
value,
localSurvey,
questionIdx,
updateQuestion,
elementIdx,
updateElement,
updateSurvey,
updateChoice,
updateMatrixLabel,
@@ -87,15 +87,15 @@ export const QuestionFormInput = ({
firstRender: externalFirstRender,
setFirstRender: externalSetFirstRender,
isExternalUrlsAllowed,
}: QuestionFormInputProps) => {
}: ElementFormInputProps) => {
const { t } = useTranslation();
const defaultLanguageCode =
localSurvey.languages.filter((lang) => lang.default)[0]?.language.code ?? "default";
const usedLanguageCode = selectedLanguageCode === defaultLanguageCode ? "default" : selectedLanguageCode;
const questions = useMemo(() => getElementsFromBlocks(localSurvey.blocks), [localSurvey.blocks]);
const elements = useMemo(() => getElementsFromBlocks(localSurvey.blocks), [localSurvey.blocks]);
const question: TSurveyElement = questions[questionIdx];
const currentElement: TSurveyElement = elements[elementIdx];
const isChoice = id.includes("choice");
const isMatrixLabelRow = id.includes("row");
const isMatrixLabelColumn = id.includes("column");
@@ -103,19 +103,19 @@ export const QuestionFormInput = ({
return isChoice || isMatrixLabelColumn || isMatrixLabelRow ? id.split("-")[0] : id;
}, [id, isChoice, isMatrixLabelColumn, isMatrixLabelRow]);
const isEndingCard = questionIdx >= questions.length;
const isWelcomeCard = questionIdx === -1;
const isEndingCard = elementIdx >= elements.length;
const isWelcomeCard = elementIdx === -1;
const index = getIndex(id, isChoice || isMatrixLabelColumn || isMatrixLabelRow);
const questionId = useMemo(() => {
const elementId = useMemo(() => {
return isWelcomeCard
? "start"
: isEndingCard
? localSurvey.endings[questionIdx - questions.length].id
: question.id;
? localSurvey.endings[elementIdx - elements.length].id
: currentElement.id;
//eslint-disable-next-line
}, [isWelcomeCard, isEndingCard, question?.id]);
const endingCard = localSurvey.endings.find((ending) => ending.id === questionId);
}, [isWelcomeCard, isEndingCard, currentElement?.id]);
const endingCard = localSurvey.endings.find((ending) => ending.id === elementId);
const surveyLanguageCodes = useMemo(
() => extractLanguageCodes(localSurvey.languages),
@@ -128,7 +128,7 @@ export const QuestionFormInput = ({
const elementText = useMemo((): TI18nString => {
if (isChoice && typeof index === "number") {
return getChoiceLabel(question, index, surveyLanguageCodes);
return getChoiceLabel(currentElement, index, surveyLanguageCodes);
}
if (isWelcomeCard) {
@@ -136,20 +136,20 @@ export const QuestionFormInput = ({
}
if (isEndingCard) {
return getEndingCardText(localSurvey, questions, id, surveyLanguageCodes, questionIdx);
return getEndingCardText(localSurvey, elements, id, surveyLanguageCodes, elementIdx);
}
if ((isMatrixLabelColumn || isMatrixLabelRow) && typeof index === "number") {
return getMatrixLabel(question, index, surveyLanguageCodes, isMatrixLabelRow ? "row" : "column");
return getMatrixLabel(currentElement, index, surveyLanguageCodes, isMatrixLabelRow ? "row" : "column");
}
return (
(question &&
(currentElement &&
(id.includes(".")
? // Handle nested properties
(question[id.split(".")[0] as keyof TSurveyElement] as any)?.[id.split(".")[1]]
(currentElement[id.split(".")[0] as keyof TSurveyElement] as any)?.[id.split(".")[1]]
: // Original behavior
(question[id as keyof TSurveyElement] as TI18nString))) ||
(currentElement[id as keyof TSurveyElement] as TI18nString))) ||
createI18nString("", surveyLanguageCodes)
);
}, [
@@ -161,15 +161,15 @@ export const QuestionFormInput = ({
isMatrixLabelRow,
isWelcomeCard,
localSurvey,
question,
questionIdx,
questions,
currentElement,
elementIdx,
elements,
surveyLanguageCodes,
]);
const [text, setText] = useState(elementText);
const [showImageUploader, setShowImageUploader] = useState<boolean>(
determineImageUploaderVisibility(questionIdx, questions)
determineImageUploaderVisibility(elementIdx, elements)
);
const highlightContainerRef = useRef<HTMLInputElement>(null);
@@ -215,25 +215,25 @@ export const QuestionFormInput = ({
[index, isMatrixLabelRow, updateMatrixLabel]
);
const updateQuestionDetails = useCallback(
const updateElementDetails = useCallback(
(translatedText: TI18nString) => {
if (updateQuestion) {
if (updateElement) {
// Handle nested properties if id contains a dot
if (id.includes(".")) {
const [parent, child] = id.split(".");
updateQuestion(questionIdx, {
updateElement(elementIdx, {
[parent]: {
...question[parent],
...currentElement[parent],
[child]: translatedText,
},
});
} else {
// Original behavior for non-nested properties
updateQuestion(questionIdx, { [id]: translatedText });
updateElement(elementIdx, { [id]: translatedText });
}
}
},
[id, questionIdx, updateQuestion, question]
[id, elementIdx, updateElement, currentElement]
);
const handleUpdate = useCallback(
@@ -247,7 +247,7 @@ export const QuestionFormInput = ({
} else if (isMatrixLabelRow || isMatrixLabelColumn) {
updateMatrixLabelDetails(translatedText);
} else {
updateQuestionDetails(translatedText);
updateElementDetails(translatedText);
}
},
[
@@ -259,7 +259,7 @@ export const QuestionFormInput = ({
isWelcomeCard,
updateChoiceDetails,
updateMatrixLabelDetails,
updateQuestionDetails,
updateElementDetails,
updateSurveyDetails,
]
);
@@ -275,14 +275,14 @@ export const QuestionFormInput = ({
if (isWelcomeCard) return localSurvey.welcomeCard.fileUrl;
if (isEndingCard) {
if (endingCard && endingCard.type === "endScreen") return endingCard.imageUrl;
} else return question.imageUrl;
} else return currentElement.imageUrl;
};
const getVideoUrl = (): string | undefined => {
if (isWelcomeCard) return localSurvey.welcomeCard.videoUrl;
if (isEndingCard) {
if (endingCard && endingCard.type === "endScreen") return endingCard.videoUrl;
} else return question.videoUrl;
} else return currentElement.videoUrl;
};
const debouncedHandleUpdate = useMemo(() => debounce((value) => handleUpdate(value), 100), [handleUpdate]);
@@ -297,35 +297,36 @@ export const QuestionFormInput = ({
const renderRemoveDescriptionButton = () => {
if (
question &&
(question.type === TSurveyElementTypeEnum.CTA || question.type === TSurveyElementTypeEnum.Consent)
currentElement &&
(currentElement.type === TSurveyElementTypeEnum.CTA ||
currentElement.type === TSurveyElementTypeEnum.Consent)
) {
return false;
}
if (id === "subheader") {
return !!question?.subheader || (endingCard?.type === "endScreen" && !!endingCard?.subheader);
return !!currentElement?.subheader || (endingCard?.type === "endScreen" && !!endingCard?.subheader);
}
return false;
};
const getIsRequiredToggleDisabled = (): boolean => {
if (!question) return false;
if (!currentElement) return false;
// CTA elements should always have the required toggle disabled
if (question.type === TSurveyElementTypeEnum.CTA) {
if (currentElement.type === TSurveyElementTypeEnum.CTA) {
return true;
}
if (question.type === TSurveyElementTypeEnum.Address) {
if (currentElement.type === TSurveyElementTypeEnum.Address) {
const allFieldsAreOptional = [
question.addressLine1,
question.addressLine2,
question.city,
question.state,
question.zip,
question.country,
currentElement.addressLine1,
currentElement.addressLine2,
currentElement.city,
currentElement.state,
currentElement.zip,
currentElement.country,
]
.filter((field) => field.show)
.every((field) => !field.required);
@@ -335,24 +336,24 @@ export const QuestionFormInput = ({
}
return [
question.addressLine1,
question.addressLine2,
question.city,
question.state,
question.zip,
question.country,
currentElement.addressLine1,
currentElement.addressLine2,
currentElement.city,
currentElement.state,
currentElement.zip,
currentElement.country,
]
.filter((field) => field.show)
.some((condition) => condition.required === true);
}
if (question.type === TSurveyElementTypeEnum.ContactInfo) {
if (currentElement.type === TSurveyElementTypeEnum.ContactInfo) {
const allFieldsAreOptional = [
question.firstName,
question.lastName,
question.email,
question.phone,
question.company,
currentElement.firstName,
currentElement.lastName,
currentElement.email,
currentElement.phone,
currentElement.company,
]
.filter((field) => field.show)
.every((field) => !field.required);
@@ -361,7 +362,13 @@ export const QuestionFormInput = ({
return true;
}
return [question.firstName, question.lastName, question.email, question.phone, question.company]
return [
currentElement.firstName,
currentElement.lastName,
currentElement.email,
currentElement.phone,
currentElement.company,
]
.filter((field) => field.show)
.some((condition) => condition.required === true);
}
@@ -371,9 +378,9 @@ export const QuestionFormInput = ({
const useRichTextEditor = id === "headline" || id === "subheader" || id === "html";
// For rich text editor fields, we need either updateQuestion or updateSurvey
if (useRichTextEditor && !updateQuestion && !updateSurvey) {
throw new Error("Either updateQuestion or updateSurvey must be provided");
// For rich text editor fields, we need either updateElement or updateSurvey
if (useRichTextEditor && !updateElement && !updateSurvey) {
throw new Error("Either updateElement or updateSurvey must be provided");
}
if (useRichTextEditor) {
@@ -382,17 +389,17 @@ export const QuestionFormInput = ({
{label && (
<div className="mb-2 mt-3 flex items-center justify-between">
<Label htmlFor={id}>{label}</Label>
{id === "headline" && question && updateQuestion && (
{id === "headline" && currentElement && updateElement && (
<div className="flex items-center space-x-2">
<Label htmlFor="required-toggle" className="text-sm">
{t("environments.surveys.edit.required")}
</Label>
<Switch
id="required-toggle"
checked={question.required}
checked={currentElement.required}
disabled={getIsRequiredToggleDisabled()}
onCheckedChange={(checked) => {
updateQuestion(questionIdx, { required: checked });
updateElement(elementIdx, { required: checked });
}}
/>
</div>
@@ -402,7 +409,7 @@ export const QuestionFormInput = ({
<div className="flex flex-col gap-4" ref={animationParent}>
{showImageUploader && id === "headline" && (
<FileInput
id="question-image"
id="element-image"
allowedFileExtensions={["png", "jpeg", "jpg", "webp", "heic"]}
environmentId={localSurvey.environmentId}
onFileUpload={(url: string[] | undefined, fileType: "image" | "video") => {
@@ -413,8 +420,8 @@ export const QuestionFormInput = ({
: { imageUrl: url[0], videoUrl: undefined };
if ((isWelcomeCard || isEndingCard) && updateSurvey) {
updateSurvey(update);
} else if (updateQuestion) {
updateQuestion(questionIdx, update);
} else if (updateElement) {
updateElement(elementIdx, update);
}
}
}}
@@ -429,19 +436,19 @@ export const QuestionFormInput = ({
<div className="flex w-full items-start gap-2">
<div className="flex-1">
<LocalizedEditor
key={`${questionId}-${id}-${selectedLanguageCode}`}
key={`${elementId}-${id}-${selectedLanguageCode}`}
id={id}
value={value}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={(isWelcomeCard || isEndingCard ? updateSurvey : updateQuestion)!}
updateElement={(isWelcomeCard || isEndingCard ? updateSurvey : updateElement)!}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
firstRender={firstRender}
setFirstRender={setFirstRender}
locale={locale}
questionId={questionId}
elementId={elementId}
isCard={isWelcomeCard || isEndingCard}
autoFocus={autoFocus}
isExternalUrlsAllowed={isExternalUrlsAllowed}
@@ -476,7 +483,7 @@ export const QuestionFormInput = ({
onClick={(e) => {
e.preventDefault();
// Suppress Editor updates BEFORE calling updateQuestion to prevent race condition
// Suppress Editor updates BEFORE calling updateElement to prevent race condition
// Use ref for immediate synchronous access
if (id === "subheader") {
suppressEditorUpdatesRef.current = true;
@@ -486,8 +493,8 @@ export const QuestionFormInput = ({
updateSurvey({ subheader: undefined });
}
if (updateQuestion) {
updateQuestion(questionIdx, { subheader: undefined });
if (updateElement) {
updateElement(elementIdx, { subheader: undefined });
}
// Re-enable updates after a short delay to allow state to update
@@ -512,17 +519,17 @@ export const QuestionFormInput = ({
{label && (
<div className="mb-2 mt-3 flex items-center justify-between">
<Label htmlFor={id}>{label}</Label>
{id === "headline" && question && updateQuestion && (
{id === "headline" && currentElement && updateElement && (
<div className="flex items-center space-x-2">
<Label htmlFor="required-toggle" className="text-sm">
{t("environments.surveys.edit.required")}
</Label>
<Switch
id="required-toggle"
checked={question.required}
checked={currentElement.required}
disabled={getIsRequiredToggleDisabled()}
onCheckedChange={(checked) => {
updateQuestion(questionIdx, { required: checked });
updateElement(elementIdx, { required: checked });
}}
/>
</div>
@@ -545,7 +552,7 @@ export const QuestionFormInput = ({
return (
<RecallWrapper
localSurvey={localSurvey}
questionId={questionId}
elementId={elementId}
value={value[usedLanguageCode]}
onChange={(value, recallItems, fallbacks) => {
// Pass all values to MultiLangWrapper's onChange
@@ -581,7 +588,7 @@ export const QuestionFormInput = ({
</div>
<Input
key={`${questionId}-${id}-${usedLanguageCode}`}
key={`${elementId}-${id}-${usedLanguageCode}`}
value={
recallToHeadline(
{
@@ -627,4 +634,4 @@ export const QuestionFormInput = ({
</div>
);
};
QuestionFormInput.displayName = "QuestionFormInput";
ElementFormInput.displayName = "ElementFormInput";

View File

@@ -9,23 +9,23 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/cn";
import {
getCXQuestionTypes,
getQuestionDefaults,
getQuestionTypes,
universalQuestionPresets,
} from "@/modules/survey/lib/questions";
getCXElementTypes,
getElementDefaults,
getElementTypes,
universalElementPresets,
} from "@/modules/survey/lib/elements";
interface AddQuestionButtonProps {
addQuestion: (question: any) => void;
interface AddElementButtonProps {
addElement: (element: any) => void;
project: Project;
isCxMode: boolean;
}
export const AddQuestionButton = ({ addQuestion, project, isCxMode }: AddQuestionButtonProps) => {
export const AddElementButton = ({ addElement, project, isCxMode }: AddElementButtonProps) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [hoveredQuestionId, setHoveredQuestionId] = useState<string | null>(null);
const availableQuestionTypes = isCxMode ? getCXQuestionTypes(t) : getQuestionTypes(t);
const [hoveredElementId, setHoveredElementId] = useState<string | null>(null);
const availableElementTypes = isCxMode ? getCXElementTypes(t) : getElementTypes(t);
const [parent] = useAutoAnimate();
return (
@@ -50,31 +50,31 @@ export const AddQuestionButton = ({ addQuestion, project, isCxMode }: AddQuestio
</div>
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="justify-left flex flex-col" ref={parent}>
{availableQuestionTypes.map((questionType) => (
{availableElementTypes.map((elementType) => (
<button
type="button"
key={questionType.id}
key={elementType.id}
className="group relative mx-2 inline-flex items-center justify-between rounded p-0.5 px-4 py-2 text-sm font-medium text-slate-700 last:mb-2 hover:bg-slate-100 hover:text-slate-800"
onClick={() => {
addQuestion({
...universalQuestionPresets,
...getQuestionDefaults(questionType.id, project, t),
addElement({
...universalElementPresets,
...getElementDefaults(elementType.id, project, t),
id: createId(),
type: questionType.id,
type: elementType.id,
});
setOpen(false);
}}
onMouseEnter={() => setHoveredQuestionId(questionType.id)}
onMouseLeave={() => setHoveredQuestionId(null)}>
onMouseEnter={() => setHoveredElementId(elementType.id)}
onMouseLeave={() => setHoveredElementId(null)}>
<div className="flex items-center">
<questionType.icon className="text-brand-dark -ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
{questionType.label}
<elementType.icon className="text-brand-dark -ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
{elementType.label}
</div>
<div
className={`absolute right-4 text-xs font-light text-slate-500 transition-opacity duration-200 ${
hoveredQuestionId === questionType.id ? "opacity-100" : "opacity-0"
hoveredElementId === elementType.id ? "opacity-100" : "opacity-0"
}`}>
{questionType.description}
{elementType.description}
</div>
</button>
))}

View File

@@ -11,12 +11,12 @@ import { TSurvey } from "@formbricks/types/surveys/types";
import { addMultiLanguageLabels, extractLanguageCodes } from "@/lib/i18n/utils";
import { addElementToBlock } from "@/modules/survey/editor/lib/blocks";
import {
getCXQuestionNameMap,
getQuestionDefaults,
getQuestionIconMap,
getQuestionNameMap,
universalQuestionPresets,
} from "@/modules/survey/lib/questions";
getCXElementNameMap,
getElementDefaults,
getElementIconMap,
getElementNameMap,
universalElementPresets,
} from "@/modules/survey/lib/elements";
import { Button } from "@/modules/ui/components/button";
import {
DropdownMenu,
@@ -25,44 +25,44 @@ import {
DropdownMenuTrigger,
} from "@/modules/ui/components/dropdown-menu";
interface AddQuestionToBlockButtonProps {
interface AddElementToBlockButtonProps {
localSurvey: TSurvey;
block: TSurveyBlock;
setLocalSurvey: (survey: TSurvey) => void;
setActiveQuestionId: (questionId: string) => void;
setActiveElementId: (elementId: string) => void;
project: Project;
isCxMode: boolean;
}
export const AddQuestionToBlockButton = ({
export const AddElementToBlockButton = ({
localSurvey,
block,
setLocalSurvey,
setActiveQuestionId,
setActiveElementId,
project,
isCxMode,
}: AddQuestionToBlockButtonProps) => {
}: AddElementToBlockButtonProps) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const availableQuestionTypes = isCxMode ? getCXQuestionNameMap(t) : getQuestionNameMap(t);
const QUESTIONS_ICON_MAP = getQuestionIconMap(t);
const availableElementTypes = isCxMode ? getCXElementNameMap(t) : getElementNameMap(t);
const ELEMENTS_ICON_MAP = getElementIconMap(t);
const handleAddQuestion = (questionType: string) => {
const handleAddElement = (elementType: string) => {
// Get language symbols and add multi-language support
const languageSymbols = extractLanguageCodes(localSurvey.languages);
const questionDefaults = getQuestionDefaults(questionType, project, t);
const questionWithLabels = addMultiLanguageLabels(
const elementDefaults = getElementDefaults(elementType, project, t);
const elementWithLabels = addMultiLanguageLabels(
{
...universalQuestionPresets,
...questionDefaults,
...universalElementPresets,
...elementDefaults,
id: createId(),
type: questionType,
type: elementType,
},
languageSymbols
);
const result = addElementToBlock(localSurvey, block.id, questionWithLabels);
const result = addElementToBlock(localSurvey, block.id, elementWithLabels);
if (!result.ok) {
toast.error(result.error.message);
@@ -72,7 +72,7 @@ export const AddQuestionToBlockButton = ({
setLocalSurvey(result.data);
setOpen(false);
setActiveQuestionId(questionWithLabels.id);
setActiveElementId(elementWithLabels.id);
};
return (
@@ -88,9 +88,9 @@ export const AddQuestionToBlockButton = ({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{Object.entries(availableQuestionTypes).map(([type, name]) => (
<DropdownMenuItem key={type} className="min-h-8" onClick={() => handleAddQuestion(type)}>
{QUESTIONS_ICON_MAP[type]}
{Object.entries(availableElementTypes).map(([type, name]) => (
<DropdownMenuItem key={type} className="min-h-8" onClick={() => handleAddElement(type)}>
{ELEMENTS_ICON_MAP[type]}
<span className="ml-2">{name}</span>
</DropdownMenuItem>
))}

View File

@@ -8,15 +8,15 @@ import { TSurveyAddressElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { Button } from "@/modules/ui/components/button";
import { QuestionToggleTable } from "@/modules/ui/components/question-toggle-table";
import { ElementToggleTable } from "@/modules/ui/components/element-toggle-table";
interface AddressQuestionFormProps {
interface AddressElementFormProps {
localSurvey: TSurvey;
question: TSurveyAddressElement;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyAddressElement>) => void;
element: TSurveyAddressElement;
elementIdx: number;
updateElement: (elementIdx: number, updatedAttributes: Partial<TSurveyAddressElement>) => void;
isInvalid: boolean;
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
@@ -25,10 +25,10 @@ interface AddressQuestionFormProps {
isExternalUrlsAllowed?: boolean;
}
export const AddressQuestionForm = ({
question,
questionIdx,
updateQuestion,
export const AddressElementForm = ({
element,
elementIdx,
updateElement,
isInvalid,
localSurvey,
selectedLanguageCode,
@@ -36,115 +36,108 @@ export const AddressQuestionForm = ({
locale,
isStorageConfigured = true,
isExternalUrlsAllowed,
}: AddressQuestionFormProps): JSX.Element => {
}: AddressElementFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
const { t } = useTranslation();
const fields = [
{
id: "addressLine1",
label: t("environments.surveys.edit.address_line_1"),
...question.addressLine1,
...element.addressLine1,
},
{
id: "addressLine2",
label: t("environments.surveys.edit.address_line_2"),
...question.addressLine2,
...element.addressLine2,
},
{
id: "city",
label: t("environments.surveys.edit.city"),
...question.city,
...element.city,
},
{
id: "state",
label: t("environments.surveys.edit.state"),
...question.state,
...element.state,
},
{
id: "zip",
label: t("environments.surveys.edit.zip"),
...question.zip,
...element.zip,
},
{
id: "country",
label: t("environments.surveys.edit.country"),
...question.country,
...element.country,
},
];
useEffect(() => {
const allFieldsAreOptional = [
question.addressLine1,
question.addressLine2,
question.city,
question.state,
question.zip,
question.country,
element.addressLine1,
element.addressLine2,
element.city,
element.state,
element.zip,
element.country,
]
.filter((field) => field.show)
.every((field) => !field.required);
updateQuestion(questionIdx, { required: !allFieldsAreOptional });
updateElement(elementIdx, { required: !allFieldsAreOptional });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
question.addressLine1,
question.addressLine2,
question.city,
question.state,
question.zip,
question.country,
]);
}, [element.addressLine1, element.addressLine2, element.city, element.state, element.zip, element.country]);
const [parent] = useAutoAnimate();
return (
<form>
<QuestionFormInput
<ElementFormInput
id="headline"
value={question.headline}
value={element.headline}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.headline?.default || question.headline.default.trim() === ""}
autoFocus={!element.headline?.default || element.headline.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
<div ref={parent}>
{question.subheader !== undefined && (
{element.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
<ElementFormInput
id="subheader"
value={question.subheader}
value={element.subheader}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.subheader?.default || question.subheader.default.trim() === ""}
autoFocus={!element.subheader?.default || element.subheader.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
</div>
</div>
)}
{question.subheader === undefined && (
{element.subheader === undefined && (
<Button
size="sm"
variant="secondary"
className="mt-4"
type="button"
onClick={() => {
updateQuestion(questionIdx, {
updateElement(elementIdx, {
subheader: createI18nString("", surveyLanguageCodes),
});
}}>
@@ -153,13 +146,13 @@ export const AddressQuestionForm = ({
</Button>
)}
<QuestionToggleTable
<ElementToggleTable
type="address"
fields={fields}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}

View File

@@ -2,41 +2,41 @@ import { TSurveyBlockLogic } from "@formbricks/types/surveys/blocks";
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { OptionIds } from "@/modules/survey/editor/components/option-ids";
import { UpdateQuestionId } from "@/modules/survey/editor/components/update-question-id";
import { UpdateElementId } from "@/modules/survey/editor/components/update-element-id";
interface AdvancedSettingsProps {
question: TSurveyElement;
questionIdx: number;
element: TSurveyElement;
elementIdx: number;
localSurvey: TSurvey;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
updateBlockLogic: (questionIdx: number, logic: TSurveyBlockLogic[]) => void;
updateBlockLogicFallback: (questionIdx: number, logicFallback: string | undefined) => void;
updateElement: (elementIdx: number, updatedAttributes: any) => void;
updateBlockLogic: (elementIdx: number, logic: TSurveyBlockLogic[]) => void;
updateBlockLogicFallback: (elementIdx: number, logicFallback: string | undefined) => void;
selectedLanguageCode: string;
}
export const AdvancedSettings = ({
question,
questionIdx,
element,
elementIdx,
localSurvey,
updateQuestion,
updateElement,
selectedLanguageCode,
}: AdvancedSettingsProps) => {
const showOptionIds =
question.type === TSurveyElementTypeEnum.PictureSelection ||
question.type === TSurveyElementTypeEnum.MultipleChoiceSingle ||
question.type === TSurveyElementTypeEnum.MultipleChoiceMulti ||
question.type === TSurveyElementTypeEnum.Ranking;
element.type === TSurveyElementTypeEnum.PictureSelection ||
element.type === TSurveyElementTypeEnum.MultipleChoiceSingle ||
element.type === TSurveyElementTypeEnum.MultipleChoiceMulti ||
element.type === TSurveyElementTypeEnum.Ranking;
return (
<div className="flex flex-col gap-4">
<UpdateQuestionId
question={question}
questionIdx={questionIdx}
<UpdateElementId
element={element}
elementIdx={elementIdx}
localSurvey={localSurvey}
updateQuestion={updateQuestion}
updateElement={updateElement}
/>
{showOptionIds && <OptionIds question={question} selectedLanguageCode={selectedLanguageCode} />}
{showOptionIds && <OptionIds element={element} selectedLanguageCode={selectedLanguageCode} />}
</div>
);
};

View File

@@ -16,28 +16,28 @@ import { getTextContent } from "@formbricks/types/surveys/validation";
import { TUserLocale } from "@formbricks/types/user";
import { cn } from "@/lib/cn";
import { recallToHeadline } from "@/lib/utils/recall";
import { AddQuestionToBlockButton } from "@/modules/survey/editor/components/add-question-to-block-button";
import { AddressQuestionForm } from "@/modules/survey/editor/components/address-question-form";
import { AddElementToBlockButton } from "@/modules/survey/editor/components/add-element-to-block-button";
import { AddressElementForm } from "@/modules/survey/editor/components/address-element-form";
import { AdvancedSettings } from "@/modules/survey/editor/components/advanced-settings";
import { BlockMenu } from "@/modules/survey/editor/components/block-menu";
import { BlockSettings } from "@/modules/survey/editor/components/block-settings";
import { CalQuestionForm } from "@/modules/survey/editor/components/cal-question-form";
import { ConsentQuestionForm } from "@/modules/survey/editor/components/consent-question-form";
import { ContactInfoQuestionForm } from "@/modules/survey/editor/components/contact-info-question-form";
import { CTAQuestionForm } from "@/modules/survey/editor/components/cta-question-form";
import { DateQuestionForm } from "@/modules/survey/editor/components/date-question-form";
import { CalElementForm } from "@/modules/survey/editor/components/cal-element-form";
import { ConsentElementForm } from "@/modules/survey/editor/components/consent-element-form";
import { ContactInfoElementForm } from "@/modules/survey/editor/components/contact-info-element-form";
import { CTAElementForm } from "@/modules/survey/editor/components/cta-element-form";
import { DateElementForm } from "@/modules/survey/editor/components/date-element-form";
import { EditorCardMenu } from "@/modules/survey/editor/components/editor-card-menu";
import { FileUploadQuestionForm } from "@/modules/survey/editor/components/file-upload-question-form";
import { MatrixQuestionForm } from "@/modules/survey/editor/components/matrix-question-form";
import { MultipleChoiceQuestionForm } from "@/modules/survey/editor/components/multiple-choice-question-form";
import { NPSQuestionForm } from "@/modules/survey/editor/components/nps-question-form";
import { OpenQuestionForm } from "@/modules/survey/editor/components/open-question-form";
import { FileUploadElementForm } from "@/modules/survey/editor/components/file-upload-element-form";
import { MatrixElementForm } from "@/modules/survey/editor/components/matrix-element-form";
import { MultipleChoiceElementForm } from "@/modules/survey/editor/components/multiple-choice-element-form";
import { NPSElementForm } from "@/modules/survey/editor/components/nps-element-form";
import { OpenElementForm } from "@/modules/survey/editor/components/open-element-form";
import { PictureSelectionForm } from "@/modules/survey/editor/components/picture-selection-form";
import { RankingQuestionForm } from "@/modules/survey/editor/components/ranking-question-form";
import { RatingQuestionForm } from "@/modules/survey/editor/components/rating-question-form";
import { RankingElementForm } from "@/modules/survey/editor/components/ranking-element-form";
import { RatingElementForm } from "@/modules/survey/editor/components/rating-element-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 { getElementIconMap, getTSurveyElementTypeEnumName } from "@/modules/survey/lib/elements";
import { Alert, AlertButton, AlertTitle } from "@/modules/ui/components/alert";
interface BlockCardProps {
@@ -45,25 +45,25 @@ interface BlockCardProps {
project: Project;
block: TSurveyBlock;
blockIdx: number;
moveQuestion: (questionIndex: number, up: boolean) => void;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
updateBlockLogic: (questionIdx: number, logic: TSurveyBlockLogic[]) => void;
updateBlockLogicFallback: (questionIdx: number, logicFallback: string | undefined) => void;
moveElement: (elementIdx: number, up: boolean) => void;
updateElement: (elementIdx: number, updatedAttributes: any) => void;
updateBlockLogic: (elementIdx: number, logic: TSurveyBlockLogic[]) => void;
updateBlockLogicFallback: (elementIdx: number, logicFallback: string | undefined) => void;
updateBlockButtonLabel: (
blockIndex: number,
labelKey: "buttonLabel" | "backButtonLabel",
labelValue: TI18nString | undefined
) => void;
deleteQuestion: (questionIdx: number) => void;
duplicateQuestion: (questionIdx: number) => void;
activeQuestionId: string | null;
setActiveQuestionId: (questionId: string | null) => void;
lastQuestion: boolean;
deleteElement: (elementIdx: number) => void;
duplicateElement: (elementIdx: number) => void;
activeElementId: string | null;
setActiveElementId: (elementId: string | null) => void;
lastElement: boolean;
lastElementIndex: number;
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
invalidQuestions?: string[];
addQuestion: (question: any, index?: number) => void;
invalidElements?: string[];
addElement: (element: any, index?: number) => void;
isFormbricksCloud: boolean;
isCxMode: boolean;
locale: TUserLocale;
@@ -85,21 +85,21 @@ export const BlockCard = ({
project,
block,
blockIdx,
moveQuestion,
updateQuestion,
moveElement,
updateElement,
updateBlockLogic,
updateBlockLogicFallback,
updateBlockButtonLabel,
duplicateQuestion,
deleteQuestion,
activeQuestionId,
setActiveQuestionId,
lastQuestion,
duplicateElement,
deleteElement,
activeElementId,
setActiveElementId,
lastElement,
lastElementIndex,
selectedLanguageCode,
setSelectedLanguageCode,
invalidQuestions,
addQuestion,
invalidElements,
addElement,
isFormbricksCloud,
isCxMode,
locale,
@@ -119,15 +119,15 @@ export const BlockCard = ({
id: block.id,
});
const { t } = useTranslation();
const QUESTIONS_ICON_MAP = getQuestionIconMap(t);
const ELEMENTS_ICON_MAP = getElementIconMap(t);
const hasMultipleElements = block.elements.length > 1;
const blockLogic = block.logic ?? [];
// Check if any element in this block is currently active
const isBlockOpen = block.elements.some((element) => element.id === activeQuestionId);
const isBlockOpen = block.elements.some((element) => element.id === activeElementId);
const hasInvalidElement = block.elements.some((element) => invalidQuestions?.includes(element.id));
const hasInvalidElement = block.elements.some((element) => invalidElements?.includes(element.id));
// 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
@@ -160,7 +160,7 @@ export const BlockCard = ({
if (headlineText) {
return formatTextWithSlashes(getTextContent(headlineText ?? ""));
}
return getTSurveyQuestionTypeEnumName(element.type, t);
return getTSurveyElementTypeEnumName(element.type, t);
};
const shouldShowCautionAlert = (elementType: TSurveyElementTypeEnum): boolean => {
@@ -178,19 +178,19 @@ export const BlockCard = ({
);
};
const renderElementForm = (element: TSurveyElement, questionIdx: number) => {
const renderElementForm = (element: TSurveyElement, elementIdx: number) => {
switch (element.type) {
case TSurveyElementTypeEnum.OpenText:
return (
<OpenQuestionForm
<OpenElementForm
localSurvey={localSurvey}
question={element}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
element={element}
elementIdx={elementIdx}
updateElement={updateElement}
lastElement={lastElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
isInvalid={invalidElements ? invalidElements.includes(element.id) : false}
locale={locale}
isStorageConfigured={isStorageConfigured}
isExternalUrlsAllowed={isExternalUrlsAllowed}
@@ -198,14 +198,14 @@ export const BlockCard = ({
);
case TSurveyElementTypeEnum.MultipleChoiceSingle:
return (
<MultipleChoiceQuestionForm
<MultipleChoiceElementForm
localSurvey={localSurvey}
question={element}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
element={element}
elementIdx={elementIdx}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
isInvalid={invalidElements ? invalidElements.includes(element.id) : false}
locale={locale}
isStorageConfigured={isStorageConfigured}
isExternalUrlsAllowed={isExternalUrlsAllowed}
@@ -213,14 +213,14 @@ export const BlockCard = ({
);
case TSurveyElementTypeEnum.MultipleChoiceMulti:
return (
<MultipleChoiceQuestionForm
<MultipleChoiceElementForm
localSurvey={localSurvey}
question={element}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
element={element}
elementIdx={elementIdx}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
isInvalid={invalidElements ? invalidElements.includes(element.id) : false}
locale={locale}
isStorageConfigured={isStorageConfigured}
isExternalUrlsAllowed={isExternalUrlsAllowed}
@@ -228,14 +228,14 @@ export const BlockCard = ({
);
case TSurveyElementTypeEnum.NPS:
return (
<NPSQuestionForm
<NPSElementForm
localSurvey={localSurvey}
question={element}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
element={element}
elementIdx={elementIdx}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
isInvalid={invalidElements ? invalidElements.includes(element.id) : false}
locale={locale}
isStorageConfigured={isStorageConfigured}
isExternalUrlsAllowed={isExternalUrlsAllowed}
@@ -243,15 +243,15 @@ export const BlockCard = ({
);
case TSurveyElementTypeEnum.CTA:
return (
<CTAQuestionForm
<CTAElementForm
localSurvey={localSurvey}
question={element}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
element={element}
elementIdx={elementIdx}
updateElement={updateElement}
lastElement={lastElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
isInvalid={invalidElements ? invalidElements.includes(element.id) : false}
locale={locale}
isStorageConfigured={isStorageConfigured}
isExternalUrlsAllowed={isExternalUrlsAllowed}
@@ -259,15 +259,15 @@ export const BlockCard = ({
);
case TSurveyElementTypeEnum.Rating:
return (
<RatingQuestionForm
<RatingElementForm
localSurvey={localSurvey}
question={element}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
element={element}
elementIdx={elementIdx}
updateElement={updateElement}
lastElement={lastElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
isInvalid={invalidElements ? invalidElements.includes(element.id) : false}
locale={locale}
isStorageConfigured={isStorageConfigured}
isExternalUrlsAllowed={isExternalUrlsAllowed}
@@ -275,14 +275,14 @@ export const BlockCard = ({
);
case TSurveyElementTypeEnum.Consent:
return (
<ConsentQuestionForm
<ConsentElementForm
localSurvey={localSurvey}
question={element}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
element={element}
elementIdx={elementIdx}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
isInvalid={invalidElements ? invalidElements.includes(element.id) : false}
locale={locale}
isStorageConfigured={isStorageConfigured}
isExternalUrlsAllowed={isExternalUrlsAllowed}
@@ -290,14 +290,14 @@ export const BlockCard = ({
);
case TSurveyElementTypeEnum.Date:
return (
<DateQuestionForm
<DateElementForm
localSurvey={localSurvey}
question={element}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
element={element}
elementIdx={elementIdx}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
isInvalid={invalidElements ? invalidElements.includes(element.id) : false}
locale={locale}
isStorageConfigured={isStorageConfigured}
isExternalUrlsAllowed={isExternalUrlsAllowed}
@@ -307,27 +307,27 @@ export const BlockCard = ({
return (
<PictureSelectionForm
localSurvey={localSurvey}
question={element}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
element={element}
elementIdx={elementIdx}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
isInvalid={invalidElements ? invalidElements.includes(element.id) : false}
locale={locale}
isStorageConfigured={isStorageConfigured}
/>
);
case TSurveyElementTypeEnum.FileUpload:
return (
<FileUploadQuestionForm
<FileUploadElementForm
localSurvey={localSurvey}
project={project}
question={element}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
element={element}
elementIdx={elementIdx}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
isInvalid={invalidElements ? invalidElements.includes(element.id) : false}
isFormbricksCloud={isFormbricksCloud}
locale={locale}
isStorageConfigured={isStorageConfigured}
@@ -336,15 +336,15 @@ export const BlockCard = ({
);
case TSurveyElementTypeEnum.Cal:
return (
<CalQuestionForm
<CalElementForm
localSurvey={localSurvey}
question={element}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
element={element}
elementIdx={elementIdx}
updateElement={updateElement}
lastElement={lastElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
isInvalid={invalidElements ? invalidElements.includes(element.id) : false}
locale={locale}
isStorageConfigured={isStorageConfigured}
isExternalUrlsAllowed={isExternalUrlsAllowed}
@@ -352,14 +352,14 @@ export const BlockCard = ({
);
case TSurveyElementTypeEnum.Matrix:
return (
<MatrixQuestionForm
<MatrixElementForm
localSurvey={localSurvey}
question={element}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
element={element}
elementIdx={elementIdx}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
isInvalid={invalidElements ? invalidElements.includes(element.id) : false}
locale={locale}
isStorageConfigured={isStorageConfigured}
isExternalUrlsAllowed={isExternalUrlsAllowed}
@@ -367,14 +367,14 @@ export const BlockCard = ({
);
case TSurveyElementTypeEnum.Address:
return (
<AddressQuestionForm
<AddressElementForm
localSurvey={localSurvey}
question={element}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
element={element}
elementIdx={elementIdx}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
isInvalid={invalidElements ? invalidElements.includes(element.id) : false}
locale={locale}
isStorageConfigured={isStorageConfigured}
isExternalUrlsAllowed={isExternalUrlsAllowed}
@@ -382,14 +382,14 @@ export const BlockCard = ({
);
case TSurveyElementTypeEnum.Ranking:
return (
<RankingQuestionForm
<RankingElementForm
localSurvey={localSurvey}
question={element}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
element={element}
elementIdx={elementIdx}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
isInvalid={invalidElements ? invalidElements.includes(element.id) : false}
locale={locale}
isStorageConfigured={isStorageConfigured}
isExternalUrlsAllowed={isExternalUrlsAllowed}
@@ -397,15 +397,15 @@ export const BlockCard = ({
);
case TSurveyElementTypeEnum.ContactInfo:
return (
<ContactInfoQuestionForm
<ContactInfoElementForm
localSurvey={localSurvey}
question={element}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
element={element}
elementIdx={elementIdx}
updateElement={updateElement}
lastElement={lastElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false}
isInvalid={invalidElements ? invalidElements.includes(element.id) : false}
locale={locale}
isStorageConfigured={isStorageConfigured}
isExternalUrlsAllowed={isExternalUrlsAllowed}
@@ -422,8 +422,8 @@ export const BlockCard = ({
zIndex: isDragging ? 10 : 1,
};
const blockQuestionCount = block.elements.length;
const blockQuestionCountText = blockQuestionCount === 1 ? "question" : "questions";
const blockElementsCount = block.elements.length;
const blockElementsCountText = blockElementsCount === 1 ? "question" : "questions";
let blockSidebarColorClass = "";
if (isBlockInvalid) {
@@ -473,7 +473,7 @@ export const BlockCard = ({
<div>
<h4 className="text-sm font-medium text-slate-700">{block.name}</h4>
<p className="text-xs text-slate-500">
{blockQuestionCount} {blockQuestionCountText}
{blockElementsCount} {blockElementsCountText}
</p>
</div>
</div>
@@ -495,24 +495,24 @@ export const BlockCard = ({
{/* Render each element in the block */}
<div ref={elementsParent}>
{block.elements.map((element, elementIndex) => {
// Calculate the actual question index in the flattened questions array
let questionIdx = 0;
// Calculate the actual element index in the flattened elements array
let elementIdx = 0;
for (let i = 0; i < blockIdx; i++) {
questionIdx += localSurvey.blocks[i].elements.length;
elementIdx += localSurvey.blocks[i].elements.length;
}
questionIdx += elementIndex;
elementIdx += elementIndex;
const isOpen = activeQuestionId === element.id;
const isOpen = activeElementId === element.id;
return (
<div key={element.id} className={cn(elementIndex > 0 && "border-t border-slate-200")}>
<Collapsible.Root
open={isOpen}
onOpenChange={() => {
if (activeQuestionId !== element.id) {
setActiveQuestionId(element.id);
if (activeElementId !== element.id) {
setActiveElementId(element.id);
} else {
setActiveQuestionId(null);
setActiveElementId(null);
}
}}
className="w-full">
@@ -527,7 +527,7 @@ export const BlockCard = ({
<div className="flex grow">
<div className="flex grow items-center gap-3" dir="auto">
<div className="flex items-center text-slate-600">
{QUESTIONS_ICON_MAP[element.type]}
{ELEMENTS_ICON_MAP[element.type]}
</div>
<div className="flex grow flex-col justify-center">
{hasMultipleElements && (
@@ -552,13 +552,13 @@ export const BlockCard = ({
<div className="flex items-center space-x-2">
<EditorCardMenu
survey={localSurvey}
cardIdx={questionIdx}
lastCard={lastQuestion && elementIndex === lastElementIndex}
cardIdx={elementIdx}
lastCard={lastElement && elementIndex === lastElementIndex}
blockId={block.id}
elementIdx={elementIndex}
duplicateCard={duplicateQuestion}
deleteCard={deleteQuestion}
moveCard={moveQuestion}
duplicateCard={duplicateElement}
deleteCard={deleteElement}
moveCard={moveElement}
card={{
...element,
logic: block.logic,
@@ -566,11 +566,11 @@ export const BlockCard = ({
backButtonLabel: block.backButtonLabel,
}}
project={project}
updateCard={updateQuestion}
addCard={addQuestion}
updateCard={updateElement}
addCard={addElement}
addCardToBlock={addElementToBlock}
moveElementToBlock={moveElementToBlock}
cardType="question"
cardType="element"
isCxMode={isCxMode}
/>
</div>
@@ -585,7 +585,7 @@ export const BlockCard = ({
</AlertButton>
</Alert>
)}
{renderElementForm(element, questionIdx)}
{renderElementForm(element, elementIdx)}
<div className="mt-4">
<Collapsible.Root
open={openAdvanced}
@@ -611,11 +611,11 @@ export const BlockCard = ({
<div className="mt-2 flex space-x-2"></div>
) : null}
<AdvancedSettings
// TODO -- We should remove this when we can confirm that everything works fine with the survey editor, not changing this right now in this file because it would require changing the question type to the respective element type in all the question forms.
question={element}
questionIdx={questionIdx}
// TODO -- We should remove this when we can confirm that everything works fine with the survey editor, not changing this right now in this file because it would require changing the element type to the respective element type in all the element forms.
element={element}
elementIdx={elementIdx}
localSurvey={localSurvey}
updateQuestion={updateQuestion}
updateElement={updateElement}
updateBlockLogic={updateBlockLogic}
updateBlockLogicFallback={updateBlockLogicFallback}
selectedLanguageCode={selectedLanguageCode}
@@ -630,13 +630,13 @@ export const BlockCard = ({
})}
</div>
<hr className="mb-4 border-dashed border-slate-200" />
{/* Add Question to Block button */}
{/* Add Element to Block button */}
<div className="p-4 pt-0">
<AddQuestionToBlockButton
<AddElementToBlockButton
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
setActiveQuestionId={setActiveQuestionId}
setActiveElementId={setActiveElementId}
block={block}
project={project}
isCxMode={isCxMode}

View File

@@ -9,7 +9,7 @@ import { TSurveyBlock, TSurveyBlockLogic } from "@formbricks/types/surveys/block
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 { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { ConditionalLogic } from "@/modules/survey/editor/components/conditional-logic";
interface BlockSettingsProps {
@@ -81,14 +81,14 @@ export const BlockSettings = ({
<div className="mt-2 space-y-4">
<div className="flex space-x-2">
{blockIndex !== 0 && (
<QuestionFormInput
<ElementFormInput
id="backButtonLabel"
value={block.backButtonLabel}
label={t("environments.surveys.edit.back_button_label")}
localSurvey={localSurvey}
questionIdx={blockIndex}
elementIdx={blockIndex}
isInvalid={false}
updateQuestion={(_, updatedAttributes) => {
updateElement={(_, updatedAttributes) => {
if ("backButtonLabel" in updatedAttributes) {
const backButtonLabel = updatedAttributes.backButtonLabel as TI18nString;
updateBlockButtonLabel(blockIndex, "backButtonLabel", {
@@ -117,14 +117,14 @@ export const BlockSettings = ({
}}
/>
)}
<QuestionFormInput
<ElementFormInput
id="buttonLabel"
value={block.buttonLabel}
label={t("environments.surveys.edit.button_label")}
localSurvey={localSurvey}
questionIdx={blockIndex}
elementIdx={blockIndex}
isInvalid={false}
updateQuestion={(_, updatedAttributes) => {
updateElement={(_, updatedAttributes) => {
if ("buttonLabel" in updatedAttributes) {
const languageSymbols = extractLanguageCodes(localSurvey.languages ?? []);
const buttonLabel = updatedAttributes.buttonLabel as TI18nString;

View File

@@ -12,23 +12,23 @@ interface BlocksDroppableProps {
localSurvey: TSurvey;
setLocalSurvey: (survey: TSurvey) => void;
project: Project;
moveQuestion: (questionIndex: number, up: boolean) => void;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
updateBlockLogic: (questionIdx: number, logic: TSurveyBlockLogic[]) => void;
updateBlockLogicFallback: (questionIdx: number, logicFallback: string | undefined) => void;
moveElement: (elementIdx: number, up: boolean) => void;
updateElement: (elementIdx: number, updatedAttributes: any) => void;
updateBlockLogic: (elementIdx: number, logic: TSurveyBlockLogic[]) => void;
updateBlockLogicFallback: (elementIdx: number, logicFallback: string | undefined) => void;
updateBlockButtonLabel: (
blockIndex: number,
labelKey: "buttonLabel" | "backButtonLabel",
labelValue: TI18nString | undefined
) => void;
deleteQuestion: (questionIdx: number) => void;
duplicateQuestion: (questionIdx: number) => void;
activeQuestionId: string | null;
setActiveQuestionId: (questionId: string | null) => void;
deleteElement: (elementIdx: number) => void;
duplicateElement: (elementIdx: number) => void;
activeElementId: string | null;
setActiveElementId: (elementId: string | null) => void;
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
invalidQuestions: string[] | null;
addQuestion: (question: any, index?: number) => void;
invalidElements: string[] | null;
addElement: (element: any, index?: number) => void;
isFormbricksCloud: boolean;
isCxMode: boolean;
locale: TUserLocale;
@@ -44,22 +44,22 @@ interface BlocksDroppableProps {
}
export const BlocksDroppable = ({
activeQuestionId,
deleteQuestion,
duplicateQuestion,
invalidQuestions,
activeElementId,
deleteElement,
duplicateElement,
invalidElements,
localSurvey,
setLocalSurvey,
moveQuestion,
moveElement,
project,
selectedLanguageCode,
setActiveQuestionId,
setActiveElementId,
setSelectedLanguageCode,
updateQuestion,
updateElement,
updateBlockLogic,
updateBlockLogicFallback,
updateBlockButtonLabel,
addQuestion,
addElement,
isFormbricksCloud,
isCxMode,
locale,
@@ -91,21 +91,21 @@ export const BlocksDroppable = ({
project={project}
block={block}
blockIdx={blockIdx}
moveQuestion={moveQuestion}
updateQuestion={updateQuestion}
moveElement={moveElement}
updateElement={updateElement}
updateBlockLogic={updateBlockLogic}
updateBlockLogicFallback={updateBlockLogicFallback}
updateBlockButtonLabel={updateBlockButtonLabel}
duplicateQuestion={duplicateQuestion}
duplicateElement={duplicateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
deleteQuestion={deleteQuestion}
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}
lastQuestion={isLastBlock}
deleteElement={deleteElement}
activeElementId={activeElementId}
setActiveElementId={setActiveElementId}
lastElement={isLastBlock}
lastElementIndex={lastElementIndex}
invalidQuestions={invalidQuestions ?? undefined}
addQuestion={addQuestion}
invalidElements={invalidElements ?? undefined}
addElement={addElement}
isFormbricksCloud={isFormbricksCloud}
isCxMode={isCxMode}
locale={locale}

View File

@@ -7,18 +7,18 @@ import { TSurveyCalElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
interface CalQuestionFormProps {
interface CalElementFormProps {
localSurvey: TSurvey;
question: TSurveyCalElement;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyCalElement>) => void;
lastQuestion: boolean;
element: TSurveyCalElement;
elementIdx: number;
updateElement: (elementIdx: number, updatedAttributes: Partial<TSurveyCalElement>) => void;
lastElement: boolean;
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
@@ -27,26 +27,26 @@ interface CalQuestionFormProps {
isExternalUrlsAllowed?: boolean;
}
export const CalQuestionForm = ({
export const CalElementForm = ({
localSurvey,
question,
questionIdx,
updateQuestion,
element,
elementIdx,
updateElement,
selectedLanguageCode,
setSelectedLanguageCode,
isInvalid,
locale,
isStorageConfigured = true,
isExternalUrlsAllowed,
}: CalQuestionFormProps): JSX.Element => {
}: CalElementFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const [isCalHostEnabled, setIsCalHostEnabled] = useState(!!question.calHost);
const [isCalHostEnabled, setIsCalHostEnabled] = useState(!!element.calHost);
const { t } = useTranslation();
useEffect(() => {
if (!isCalHostEnabled) {
updateQuestion(questionIdx, { calHost: undefined });
updateElement(elementIdx, { calHost: undefined });
} else {
updateQuestion(questionIdx, { calHost: question.calHost ?? "cal.com" });
updateElement(elementIdx, { calHost: element.calHost ?? "cal.com" });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -54,51 +54,51 @@ export const CalQuestionForm = ({
return (
<form>
<QuestionFormInput
<ElementFormInput
id="headline"
value={question.headline}
value={element.headline}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.headline?.default || question.headline.default.trim() === ""}
autoFocus={!element.headline?.default || element.headline.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
<div>
{question.subheader !== undefined && (
{element.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
<ElementFormInput
id="subheader"
value={question.subheader}
value={element.subheader}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.subheader?.default || question.subheader.default.trim() === ""}
autoFocus={!element.subheader?.default || element.subheader.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
</div>
</div>
)}
{question.subheader === undefined && (
{element.subheader === undefined && (
<Button
size="sm"
className="mt-3"
variant="secondary"
type="button"
onClick={() => {
updateQuestion(questionIdx, {
updateElement(elementIdx, {
subheader: createI18nString("", surveyLanguageCodes),
});
}}>
@@ -113,8 +113,8 @@ export const CalQuestionForm = ({
<Input
id="calUserName"
name="calUserName"
value={question.calUserName}
onChange={(e) => updateQuestion(questionIdx, { calUserName: e.target.value })}
value={element.calUserName}
onChange={(e) => updateElement(elementIdx, { calUserName: e.target.value })}
/>
</div>
</div>
@@ -134,9 +134,9 @@ export const CalQuestionForm = ({
id="calHost"
name="calHost"
placeholder="my-cal-instance.com"
value={question.calHost}
value={element.calHost}
className="bg-white"
onChange={(e) => updateQuestion(questionIdx, { calHost: e.target.value })}
onChange={(e) => updateElement(elementIdx, { calHost: e.target.value })}
/>
</div>
</div>

View File

@@ -5,13 +5,13 @@ import { useTranslation } from "react-i18next";
import { TSurveyConsentElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
interface ConsentQuestionFormProps {
interface ConsentElementFormProps {
localSurvey: TSurvey;
question: TSurveyConsentElement;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyConsentElement>) => void;
element: TSurveyConsentElement;
elementIdx: number;
updateElement: (elementIdx: number, updatedAttributes: Partial<TSurveyConsentElement>) => void;
selectedLanguageCode: string;
setSelectedLanguageCode: (languageCode: string) => void;
isInvalid: boolean;
@@ -20,10 +20,10 @@ interface ConsentQuestionFormProps {
isExternalUrlsAllowed?: boolean;
}
export const ConsentQuestionForm = ({
question,
questionIdx,
updateQuestion,
export const ConsentElementForm = ({
element,
elementIdx,
updateElement,
isInvalid,
localSurvey,
selectedLanguageCode,
@@ -31,15 +31,15 @@ export const ConsentQuestionForm = ({
locale,
isStorageConfigured = true,
isExternalUrlsAllowed,
}: ConsentQuestionFormProps): JSX.Element => {
}: ConsentElementFormProps): JSX.Element => {
const { t } = useTranslation();
// Common props shared across all QuestionFormInput components
// Common props shared across all ElementFormInput components
const commonInputProps = {
localSurvey,
questionIdx,
elementIdx,
isInvalid,
updateQuestion,
updateElement,
selectedLanguageCode,
setSelectedLanguageCode,
locale,
@@ -49,29 +49,29 @@ export const ConsentQuestionForm = ({
return (
<form>
<QuestionFormInput
<ElementFormInput
{...commonInputProps}
id="headline"
value={question.headline}
value={element.headline}
label={t("environments.surveys.edit.question") + "*"}
autoFocus={!question.headline?.default || question.headline.default.trim() === ""}
autoFocus={!element.headline?.default || element.headline.default.trim() === ""}
/>
<div className="mt-3">
<QuestionFormInput
<ElementFormInput
{...commonInputProps}
id="subheader"
value={question.subheader}
value={element.subheader}
label={t("common.description")}
/>
</div>
<QuestionFormInput
<ElementFormInput
{...commonInputProps}
id="label"
label={t("environments.surveys.edit.checkbox_label") + "*"}
placeholder="I agree to the terms and conditions"
value={question.label}
value={element.label}
/>
</form>
);

View File

@@ -8,16 +8,16 @@ import { TSurveyContactInfoElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { Button } from "@/modules/ui/components/button";
import { QuestionToggleTable } from "@/modules/ui/components/question-toggle-table";
import { ElementToggleTable } from "@/modules/ui/components/element-toggle-table";
interface ContactInfoQuestionFormProps {
interface ContactInfoElementFormProps {
localSurvey: TSurvey;
question: TSurveyContactInfoElement;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyContactInfoElement>) => void;
lastQuestion: boolean;
element: TSurveyContactInfoElement;
elementIdx: number;
updateElement: (elementIdx: number, updatedAttributes: Partial<TSurveyContactInfoElement>) => void;
lastElement: boolean;
isInvalid: boolean;
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
@@ -26,10 +26,10 @@ interface ContactInfoQuestionFormProps {
isExternalUrlsAllowed?: boolean;
}
export const ContactInfoQuestionForm = ({
question,
questionIdx,
updateQuestion,
export const ContactInfoElementForm = ({
element,
elementIdx,
updateElement,
isInvalid,
localSurvey,
selectedLanguageCode,
@@ -37,7 +37,7 @@ export const ContactInfoQuestionForm = ({
locale,
isStorageConfigured = true,
isExternalUrlsAllowed,
}: ContactInfoQuestionFormProps): JSX.Element => {
}: ContactInfoElementFormProps): JSX.Element => {
const { t } = useTranslation();
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
@@ -45,96 +45,96 @@ export const ContactInfoQuestionForm = ({
{
id: "firstName",
label: t("environments.surveys.edit.first_name"),
...question.firstName,
...element.firstName,
},
{
id: "lastName",
label: t("environments.surveys.edit.last_name"),
...question.lastName,
...element.lastName,
},
{
id: "email",
label: t("common.email"),
...question.email,
...element.email,
},
{
id: "phone",
label: t("common.phone"),
...question.phone,
...element.phone,
},
{
id: "company",
label: t("environments.surveys.edit.company"),
...question.company,
...element.company,
},
];
useEffect(() => {
const allFieldsAreOptional = [
question.firstName,
question.lastName,
question.email,
question.phone,
question.company,
element.firstName,
element.lastName,
element.email,
element.phone,
element.company,
]
.filter((field) => field.show)
.every((field) => !field.required);
updateQuestion(questionIdx, { required: !allFieldsAreOptional });
updateElement(elementIdx, { required: !allFieldsAreOptional });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [question.firstName, question.lastName, question.email, question.phone, question.company]);
}, [element.firstName, element.lastName, element.email, element.phone, element.company]);
const [parent] = useAutoAnimate();
return (
<form>
<QuestionFormInput
<ElementFormInput
id="headline"
value={question.headline}
value={element.headline}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.headline?.default || question.headline.default.trim() === ""}
autoFocus={!element.headline?.default || element.headline.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
<div ref={parent}>
{question.subheader !== undefined && (
{element.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
<ElementFormInput
id="subheader"
value={question.subheader}
value={element.subheader}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.subheader?.default || question.subheader.default.trim() === ""}
autoFocus={!element.subheader?.default || element.subheader.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
</div>
</div>
)}
{question.subheader === undefined && (
{element.subheader === undefined && (
<Button
size="sm"
variant="secondary"
className="mt-4"
type="button"
onClick={() => {
updateQuestion(questionIdx, {
updateElement(elementIdx, {
subheader: createI18nString("", surveyLanguageCodes),
});
}}>
@@ -143,13 +143,13 @@ export const ContactInfoQuestionForm = ({
</Button>
)}
<QuestionToggleTable
<ElementToggleTable
type="contact"
fields={fields}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}

View File

@@ -5,17 +5,17 @@ import { useTranslation } from "react-i18next";
import { TSurveyCTAElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
interface CTAQuestionFormProps {
interface CTAElementFormProps {
localSurvey: TSurvey;
question: TSurveyCTAElement;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyCTAElement>) => void;
lastQuestion: boolean;
element: TSurveyCTAElement;
elementIdx: number;
updateElement: (elementIdx: number, updatedAttributes: Partial<TSurveyCTAElement>) => void;
lastElement: boolean;
selectedLanguageCode: string;
setSelectedLanguageCode: (languageCode: string) => void;
isInvalid: boolean;
@@ -24,11 +24,11 @@ interface CTAQuestionFormProps {
isExternalUrlsAllowed?: boolean;
}
export const CTAQuestionForm = ({
question,
questionIdx,
updateQuestion,
lastQuestion,
export const CTAElementForm = ({
element,
elementIdx,
updateElement,
lastElement,
isInvalid,
localSurvey,
selectedLanguageCode,
@@ -36,36 +36,36 @@ export const CTAQuestionForm = ({
locale,
isStorageConfigured = true,
isExternalUrlsAllowed,
}: CTAQuestionFormProps): JSX.Element => {
}: CTAElementFormProps): JSX.Element => {
const { t } = useTranslation();
return (
<form>
<QuestionFormInput
<ElementFormInput
id="headline"
value={question.headline}
value={element.headline}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.headline?.default || question.headline.default.trim() === ""}
autoFocus={!element.headline?.default || element.headline.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
<div className="mt-3">
<QuestionFormInput
<ElementFormInput
id="subheader"
value={question.subheader}
value={element.subheader}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
@@ -76,24 +76,24 @@ export const CTAQuestionForm = ({
<div className="mt-3 flex-1">
<AdvancedOptionToggle
isChecked={question.buttonExternal}
onToggle={() => updateQuestion(questionIdx, { buttonExternal: !question.buttonExternal })}
isChecked={element.buttonExternal}
onToggle={() => updateElement(elementIdx, { buttonExternal: !element.buttonExternal })}
htmlId="buttonExternal"
title={t("environments.surveys.edit.button_external")}
description={t("environments.surveys.edit.button_external_description")}
childBorder
customContainerClass="p-0 mt-4">
<div className="flex flex-1 flex-col gap-2 px-4 pb-4 pt-1">
<QuestionFormInput
<ElementFormInput
id="ctaButtonLabel"
value={question.ctaButtonLabel}
value={element.ctaButtonLabel}
label={t("environments.surveys.edit.cta_button_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
maxLength={48}
placeholder={lastQuestion ? t("common.finish") : t("common.next")}
placeholder={lastElement ? t("common.finish") : t("common.next")}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
@@ -105,9 +105,9 @@ export const CTAQuestionForm = ({
<Input
id="buttonUrl"
name="buttonUrl"
value={question.buttonUrl}
value={element.buttonUrl}
placeholder="https://website.com"
onChange={(e) => updateQuestion(questionIdx, { buttonUrl: e.target.value })}
onChange={(e) => updateElement(elementIdx, { buttonUrl: e.target.value })}
/>
</div>
</div>

View File

@@ -8,16 +8,16 @@ import { TSurveyDateElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { Button } from "@/modules/ui/components/button";
import { Label } from "@/modules/ui/components/label";
import { OptionsSwitch } from "@/modules/ui/components/options-switch";
interface IDateQuestionFormProps {
interface IDateElementFormProps {
localSurvey: TSurvey;
question: TSurveyDateElement;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyDateElement>) => void;
element: TSurveyDateElement;
elementIdx: number;
updateElement: (elementIdx: number, updatedAttributes: Partial<TSurveyDateElement>) => void;
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
@@ -41,10 +41,10 @@ const dateOptions = [
},
];
export const DateQuestionForm = ({
question,
questionIdx,
updateQuestion,
export const DateElementForm = ({
element,
elementIdx,
updateElement,
isInvalid,
localSurvey,
selectedLanguageCode,
@@ -52,59 +52,59 @@ export const DateQuestionForm = ({
locale,
isStorageConfigured = true,
isExternalUrlsAllowed,
}: IDateQuestionFormProps): JSX.Element => {
}: IDateElementFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const { t } = useTranslation();
const [parent] = useAutoAnimate();
return (
<form>
<QuestionFormInput
<ElementFormInput
id="headline"
value={question.headline}
value={element.headline}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.headline?.default || question.headline.default.trim() === ""}
autoFocus={!element.headline?.default || element.headline.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
<div ref={parent}>
{question.subheader !== undefined && (
{element.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
<ElementFormInput
id="subheader"
value={question.subheader}
value={element.subheader}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.subheader?.default || question.subheader.default.trim() === ""}
autoFocus={!element.subheader?.default || element.subheader.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
</div>
</div>
)}
{question.subheader === undefined && (
{element.subheader === undefined && (
<Button
size="sm"
className="mt-3"
variant="secondary"
type="button"
onClick={() => {
updateQuestion(questionIdx, {
updateElement(elementIdx, {
subheader: createI18nString("", surveyLanguageCodes),
});
}}>
@@ -115,13 +115,13 @@ export const DateQuestionForm = ({
</div>
<div className="mt-3">
<Label htmlFor="questionType">{t("environments.surveys.edit.date_format")}</Label>
<Label htmlFor="elementType">{t("environments.surveys.edit.date_format")}</Label>
<div className="mt-2 flex items-center">
<OptionsSwitch
options={dateOptions}
currentOption={question.format}
currentOption={element.format}
handleOptionChange={(value: "M-d-y" | "d-M-y" | "y-M-d") =>
updateQuestion(questionIdx, { format: value })
updateElement(elementIdx, { format: value })
}
/>
</div>

View File

@@ -9,12 +9,7 @@ import { useMemo, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TSurveyQuota } from "@formbricks/types/quota";
import {
TSurvey,
TSurveyEndScreenCard,
TSurveyQuestionId,
TSurveyRedirectUrlCard,
} from "@formbricks/types/surveys/types";
import { TSurvey, TSurveyEndScreenCard, TSurveyRedirectUrlCard } from "@formbricks/types/surveys/types";
import { getTextContent } from "@formbricks/types/surveys/validation";
import { TUserLocale } from "@formbricks/types/user";
import { cn } from "@/lib/cn";
@@ -35,8 +30,8 @@ interface EditEndingCardProps {
localSurvey: TSurvey;
endingCardIndex: number;
setLocalSurvey: React.Dispatch<React.SetStateAction<TSurvey>>;
setActiveQuestionId: (id: string | null) => void;
activeQuestionId: TSurveyQuestionId | null;
setActiveElementId: (id: string | null) => void;
activeElementId: string | null;
isInvalid: boolean;
selectedLanguageCode: string;
setSelectedLanguageCode: (languageCode: string) => void;
@@ -52,8 +47,8 @@ export const EditEndingCard = ({
localSurvey,
endingCardIndex,
setLocalSurvey,
setActiveQuestionId,
activeQuestionId,
setActiveElementId,
activeElementId,
isInvalid,
selectedLanguageCode,
setSelectedLanguageCode,
@@ -90,13 +85,13 @@ export const EditEndingCard = ({
id: endingCard.id,
});
let open = activeQuestionId === endingCard.id;
let open = activeElementId === endingCard.id;
const setOpen = (e) => {
if (e) {
setActiveQuestionId(endingCard.id);
setActiveElementId(endingCard.id);
} else {
setActiveQuestionId(null);
setActiveElementId(null);
}
};

View File

@@ -5,10 +5,10 @@ import { Hand } from "lucide-react";
import { usePathname } from "next/navigation";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { TSurvey, TSurveyQuestionId, TSurveyWelcomeCard } from "@formbricks/types/surveys/types";
import { TSurvey, TSurveyWelcomeCard } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { cn } from "@/lib/cn";
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { FileInput } from "@/modules/ui/components/file-input";
import { Label } from "@/modules/ui/components/label";
import { Switch } from "@/modules/ui/components/switch";
@@ -16,8 +16,8 @@ import { Switch } from "@/modules/ui/components/switch";
interface EditWelcomeCardProps {
localSurvey: TSurvey;
setLocalSurvey: (survey: TSurvey) => void;
setActiveQuestionId: (id: string | null) => void;
activeQuestionId: TSurveyQuestionId | null;
setActiveElementId: (id: string | null) => void;
activeElementId: string | null;
isInvalid: boolean;
selectedLanguageCode: string;
setSelectedLanguageCode: (languageCode: string) => void;
@@ -28,8 +28,8 @@ interface EditWelcomeCardProps {
export const EditWelcomeCard = ({
localSurvey,
setLocalSurvey,
setActiveQuestionId,
activeQuestionId,
setActiveElementId,
activeElementId,
isInvalid,
selectedLanguageCode,
setSelectedLanguageCode,
@@ -43,13 +43,13 @@ export const EditWelcomeCard = ({
const path = usePathname();
const environmentId = path?.split("/environments/")[1]?.split("/")[0];
let open = activeQuestionId == "start";
let open = activeElementId == "start";
const setOpen = (e) => {
if (e) {
setActiveQuestionId("start");
setActiveElementId("start");
} else {
setActiveQuestionId(null);
setActiveElementId(null);
}
};
@@ -126,12 +126,12 @@ export const EditWelcomeCard = ({
/>
</div>
<div className="mt-3">
<QuestionFormInput
<ElementFormInput
id="headline"
value={localSurvey.welcomeCard.headline}
label={t("common.note") + "*"}
localSurvey={localSurvey}
questionIdx={-1}
elementIdx={-1}
isInvalid={isInvalid}
updateSurvey={updateSurvey}
selectedLanguageCode={selectedLanguageCode}
@@ -143,12 +143,12 @@ export const EditWelcomeCard = ({
/>
</div>
<div className="mt-3">
<QuestionFormInput
<ElementFormInput
id="subheader"
value={localSurvey.welcomeCard.subheader}
label={t("environments.surveys.edit.welcome_message")}
localSurvey={localSurvey}
questionIdx={-1}
elementIdx={-1}
isInvalid={isInvalid}
updateSurvey={updateSurvey}
selectedLanguageCode={selectedLanguageCode}
@@ -163,11 +163,11 @@ export const EditWelcomeCard = ({
<div className="mt-3 flex justify-between gap-8">
<div className="flex w-full space-x-2">
<div className="w-full">
<QuestionFormInput
<ElementFormInput
id="buttonLabel"
value={localSurvey.welcomeCard.buttonLabel}
localSurvey={localSurvey}
questionIdx={-1}
elementIdx={-1}
maxLength={48}
placeholder={t("common.next")}
isInvalid={isInvalid}

View File

@@ -11,11 +11,11 @@ import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/survey
import { TSurvey, TSurveyEndScreenCard, TSurveyRedirectUrlCard } from "@formbricks/types/surveys/types";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import {
getCXQuestionNameMap,
getQuestionDefaults,
getQuestionIconMap,
getQuestionNameMap,
} from "@/modules/survey/lib/questions";
getCXElementNameMap,
getElementDefaults,
getElementIconMap,
getElementNameMap,
} from "@/modules/survey/lib/elements";
import { Button } from "@/modules/ui/components/button";
import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal";
import {
@@ -46,10 +46,10 @@ interface EditorCardMenuProps {
moveCard: (cardIdx: number, up: boolean) => void;
card: EditorCardMenuSurveyElement | TSurveyEndScreenCard | TSurveyRedirectUrlCard;
updateCard: (cardIdx: number, updatedAttributes: any) => void;
addCard: (question: any, index?: number) => void;
addCard: (element: any, index?: number) => void;
addCardToBlock?: (element: TSurveyElement, blockId: string, afterElementIdx: number) => void;
moveElementToBlock?: (elementId: string, targetBlockId: string) => void;
cardType: "question" | "ending";
cardType: "element" | "ending";
project?: Project;
isCxMode?: boolean;
}
@@ -73,7 +73,7 @@ export const EditorCardMenu = ({
isCxMode = false,
}: EditorCardMenuProps) => {
const { t } = useTranslation();
const QUESTIONS_ICON_MAP = getQuestionIconMap(t);
const ELEMENTS_ICON_MAP = getElementIconMap(t);
const [logicWarningModal, setLogicWarningModal] = useState(false);
const [changeToType, setChangeToType] = useState(() => {
if (card.type !== "endScreen" && card.type !== "redirectToUrl") {
@@ -83,19 +83,19 @@ export const EditorCardMenu = ({
return undefined;
});
const questions = getElementsFromBlocks(survey.blocks);
const elements = getElementsFromBlocks(survey.blocks);
const isDeleteDisabled =
cardType === "question" ? questions.length === 1 : survey.type === "link" && survey.endings.length === 1;
cardType === "element" ? elements.length === 1 : survey.type === "link" && survey.endings.length === 1;
const availableQuestionTypes = isCxMode ? getCXQuestionNameMap(t) : getQuestionNameMap(t);
const availableElementTypes = isCxMode ? getCXElementNameMap(t) : getElementNameMap(t);
const changeQuestionType = (type?: TSurveyElementTypeEnum) => {
const changeElementType = (type?: TSurveyElementTypeEnum) => {
if (!type) return;
const { headline, required, subheader, imageUrl, videoUrl, buttonLabel, backButtonLabel } =
card as EditorCardMenuSurveyElement;
const questionDefaults = getQuestionDefaults(type, project, t);
const elementDefaults = getElementDefaults(type, project, t);
if (
(type === TSurveyElementTypeEnum.MultipleChoiceSingle &&
@@ -118,7 +118,7 @@ export const EditorCardMenu = ({
}
updateCard(cardIdx, {
...questionDefaults,
...elementDefaults,
type,
headline,
subheader,
@@ -131,22 +131,22 @@ export const EditorCardMenu = ({
});
};
const addQuestionCardBelow = (type: TSurveyElementTypeEnum) => {
const questionDefaults = getQuestionDefaults(type, project, t);
const addElementCardBelow = (type: TSurveyElementTypeEnum) => {
const elementDefaults = getElementDefaults(type, project, t);
const newQuestion = {
...questionDefaults,
const newElement = {
...elementDefaults,
type,
id: createId(),
required: type === TSurveyElementTypeEnum.CTA ? false : true,
};
// Add question to block or as new block
// Add element to block or as new block
if (addCardToBlock && blockId && elementIdx !== undefined) {
// Pass blockId and element index within the block
addCardToBlock(newQuestion as TSurveyElement, blockId, elementIdx);
addCardToBlock(newElement as TSurveyElement, blockId, elementIdx);
} else {
addCard(newQuestion, cardIdx + 1);
addCard(newElement, cardIdx + 1);
}
const section = document.getElementById(`${card.id}`);
@@ -158,7 +158,7 @@ export const EditorCardMenu = ({
};
const onConfirm = () => {
changeQuestionType(changeToType);
changeElementType(changeToType);
setLogicWarningModal(false);
};
@@ -229,7 +229,7 @@ export const EditorCardMenu = ({
<DropdownMenuContent>
<div className="flex flex-col">
{cardType === "question" && (
{cardType === "element" && (
<DropdownMenuSub>
<DropdownMenuSubTrigger
className="cursor-pointer text-sm text-slate-600 hover:text-slate-700"
@@ -238,7 +238,7 @@ export const EditorCardMenu = ({
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="ml-2">
{Object.entries(availableQuestionTypes).map(([type, name]) => {
{Object.entries(availableElementTypes).map(([type, name]) => {
if (type === card.type) return null;
return (
<DropdownMenuItem
@@ -250,9 +250,9 @@ export const EditorCardMenu = ({
return;
}
changeQuestionType(type as TSurveyElementTypeEnum);
changeElementType(type as TSurveyElementTypeEnum);
}}
icon={QUESTIONS_ICON_MAP[type as TSurveyElementTypeEnum]}>
icon={ELEMENTS_ICON_MAP[type as TSurveyElementTypeEnum]}>
<span className="ml-2">{name}</span>
</DropdownMenuItem>
);
@@ -271,25 +271,25 @@ export const EditorCardMenu = ({
</DropdownMenuItem>
)}
{cardType === "question" && (
{cardType === "element" && (
<DropdownMenuSub>
<DropdownMenuSubTrigger className="cursor-pointer" onClick={(e) => e.preventDefault()}>
{t("environments.surveys.edit.add_question_below")}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="ml-2">
{Object.entries(availableQuestionTypes).map(([type, name]) => {
{Object.entries(availableElementTypes).map(([type, name]) => {
return (
<DropdownMenuItem
key={type}
className="min-h-8"
onClick={(e) => {
e.stopPropagation();
if (cardType === "question") {
addQuestionCardBelow(type as TSurveyElementTypeEnum);
if (cardType === "element") {
addElementCardBelow(type as TSurveyElementTypeEnum);
}
}}>
{QUESTIONS_ICON_MAP[type as TSurveyElementTypeEnum]}
{ELEMENTS_ICON_MAP[type as TSurveyElementTypeEnum]}
<span className="ml-2">{name}</span>
</DropdownMenuItem>
);
@@ -297,7 +297,7 @@ export const EditorCardMenu = ({
</DropdownMenuSubContent>
</DropdownMenuSub>
)}
{cardType === "question" && moveElementToBlock && survey.blocks.length > 1 && (
{cardType === "element" && moveElementToBlock && survey.blocks.length > 1 && (
<DropdownMenuSub>
<DropdownMenuSubTrigger className="cursor-pointer" onClick={(e) => e.preventDefault()}>
{t("environments.surveys.edit.move_question_to_block")}

View File

@@ -14,7 +14,7 @@ import { TSurvey, TSurveyLanguage } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { cn } from "@/lib/cn";
import { createI18nString } from "@/lib/i18n/utils";
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { Button } from "@/modules/ui/components/button";
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
import { isLabelValidForAllLanguages } from "../lib/validation";
@@ -22,7 +22,7 @@ import { isLabelValidForAllLanguages } from "../lib/validation";
interface ChoiceProps {
choice: TSurveyElementChoice;
choiceIdx: number;
questionIdx: number;
elementIdx: number;
updateChoice: (choiceIdx: number, updatedAttributes: { label: TI18nString }) => void;
deleteChoice: (choiceIdx: number) => void;
addChoice: (choiceIdx: number) => void;
@@ -31,9 +31,9 @@ interface ChoiceProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
surveyLanguages: TSurveyLanguage[];
question: TSurveyMultipleChoiceElement | TSurveyRankingElement;
updateQuestion: (
questionIdx: number,
element: TSurveyMultipleChoiceElement | TSurveyRankingElement;
updateElement: (
elementIdx: number,
updatedAttributes: Partial<TSurveyMultipleChoiceElement> | Partial<TSurveyRankingElement>
) => void;
surveyLanguageCodes: string[];
@@ -41,21 +41,21 @@ interface ChoiceProps {
isStorageConfigured: boolean;
}
export const QuestionOptionChoice = ({
export const ElementOptionChoice = ({
addChoice,
choice,
choiceIdx,
deleteChoice,
isInvalid,
localSurvey,
questionIdx,
elementIdx,
selectedLanguageCode,
setSelectedLanguageCode,
surveyLanguages,
updateChoice,
question,
element,
surveyLanguageCodes,
updateQuestion,
updateElement,
locale,
isStorageConfigured,
}: ChoiceProps) => {
@@ -88,7 +88,7 @@ export const QuestionOptionChoice = ({
return t("environments.surveys.edit.option_idx", { choiceIndex: choiceIdx + 1 });
};
const normalChoice = question.choices?.filter((c) => c.id !== "other" && c.id !== "none") || [];
const normalChoice = element.choices?.filter((c) => c.id !== "other" && c.id !== "none") || [];
return (
<div className="flex w-full items-center gap-2" ref={setNodeRef} style={style}>
@@ -98,19 +98,19 @@ export const QuestionOptionChoice = ({
</div>
<div className="flex w-full space-x-2">
<QuestionFormInput
<ElementFormInput
key={choice.id}
id={`choice-${choiceIdx}`}
placeholder={getPlaceholder()}
label={""}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
value={choice.label}
updateChoice={updateChoice}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={
isInvalid && !isLabelValidForAllLanguages(question.choices?.[choiceIdx]?.label, surveyLanguages)
isInvalid && !isLabelValidForAllLanguages(element.choices?.[choiceIdx]?.label, surveyLanguages)
}
className={`${isSpecialChoice ? "border border-dashed" : ""} mt-0`}
locale={locale}
@@ -118,7 +118,7 @@ export const QuestionOptionChoice = ({
onKeyDown={(e) => {
if (e.key === "Enter" && choice.id !== "other") {
e.preventDefault();
const lastChoiceIdx = question.choices?.findLastIndex((c) => c.id !== "other") ?? -1;
const lastChoiceIdx = element.choices?.findLastIndex((c) => c.id !== "other") ?? -1;
if (choiceIdx === lastChoiceIdx) {
addChoiceAndFocus(choiceIdx);
@@ -129,7 +129,7 @@ export const QuestionOptionChoice = ({
if (e.key === "ArrowDown") {
e.preventDefault();
if (choiceIdx + 1 < (question.choices?.length ?? 0)) {
if (choiceIdx + 1 < (element.choices?.length ?? 0)) {
focusChoiceInput(choiceIdx + 1);
}
}
@@ -143,21 +143,21 @@ export const QuestionOptionChoice = ({
}}
/>
{choice.id === "other" && (
<QuestionFormInput
<ElementFormInput
id="otherOptionPlaceholder"
localSurvey={localSurvey}
placeholder={t("environments.surveys.edit.please_specify")}
label={""}
questionIdx={questionIdx}
elementIdx={elementIdx}
value={
question.otherOptionPlaceholder ??
element.otherOptionPlaceholder ??
createI18nString(t("environments.surveys.edit.please_specify"), surveyLanguageCodes)
}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={
isInvalid && !isLabelValidForAllLanguages(question.choices?.[choiceIdx]?.label, surveyLanguages)
isInvalid && !isLabelValidForAllLanguages(element.choices?.[choiceIdx]?.label, surveyLanguages)
}
className="border border-dashed"
locale={locale}

View File

@@ -29,8 +29,8 @@ import { structuredClone } from "@/lib/pollyfills/structuredClone";
import { isConditionGroup } from "@/lib/surveyLogic/utils";
import { checkForEmptyFallBackValue, extractRecallInfo } from "@/lib/utils/recall";
import { MultiLanguageCard } from "@/modules/ee/multi-language-surveys/components/multi-language-card";
import { AddElementButton } from "@/modules/survey/editor/components/add-element-button";
import { AddEndingCardButton } from "@/modules/survey/editor/components/add-ending-card-button";
import { AddQuestionButton } from "@/modules/survey/editor/components/add-question-button";
import { BlocksDroppable } from "@/modules/survey/editor/components/blocks-droppable";
import { EditEndingCard } from "@/modules/survey/editor/components/edit-ending-card";
import { EditWelcomeCard } from "@/modules/survey/editor/components/edit-welcome-card";
@@ -47,7 +47,7 @@ import {
moveElementInBlock,
updateElementInBlock,
} from "@/modules/survey/editor/lib/blocks";
import { findQuestionUsedInLogic, isUsedInQuota, isUsedInRecall } from "@/modules/survey/editor/lib/utils";
import { findElementUsedInLogic, isUsedInQuota, isUsedInRecall } from "@/modules/survey/editor/lib/utils";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import {
isEndingCardValid,
@@ -56,15 +56,15 @@ import {
validateSurveyElementsInBatch,
} from "../lib/validation";
interface QuestionsViewProps {
interface ElementsViewProps {
localSurvey: TSurvey;
setLocalSurvey: React.Dispatch<SetStateAction<TSurvey>>;
activeQuestionId: string | null;
setActiveQuestionId: (questionId: string | null) => void;
activeElementId: string | null;
setActiveElementId: (elementId: string | null) => void;
project: Project;
projectLanguages: Language[];
invalidQuestions: string[] | null;
setInvalidQuestions: React.Dispatch<SetStateAction<string[] | null>>;
invalidElements: string[] | null;
setInvalidElements: React.Dispatch<SetStateAction<string[] | null>>;
selectedLanguageCode: string;
setSelectedLanguageCode: (languageCode: string) => void;
isMultiLanguageAllowed?: boolean;
@@ -78,15 +78,15 @@ interface QuestionsViewProps {
isExternalUrlsAllowed: boolean;
}
export const QuestionsView = ({
activeQuestionId,
setActiveQuestionId,
export const ElementsView = ({
activeElementId,
setActiveElementId,
localSurvey,
setLocalSurvey,
project,
projectLanguages,
invalidQuestions,
setInvalidQuestions,
invalidElements,
setInvalidElements,
setSelectedLanguageCode,
selectedLanguageCode,
isMultiLanguageAllowed,
@@ -98,28 +98,27 @@ export const QuestionsView = ({
isStorageConfigured = true,
quotas,
isExternalUrlsAllowed,
}: QuestionsViewProps) => {
}: ElementsViewProps) => {
const { t } = useTranslation();
// Derive questions from blocks for display
const questions = useMemo(() => getElementsFromBlocks(localSurvey.blocks), [localSurvey.blocks]);
const elements = useMemo(() => getElementsFromBlocks(localSurvey.blocks), [localSurvey.blocks]);
const internalQuestionIdMap = useMemo(() => {
return questions.reduce((acc, question) => {
acc[question.id] = createId();
const internalElementIdMap = useMemo(() => {
return elements.reduce((acc, element) => {
acc[element.id] = createId();
return acc;
}, {});
}, [questions]);
}, [elements]);
const surveyLanguages = localSurvey.languages;
const getQuestionIdFromBlockId = (block: TSurveyBlock): string => block.elements[0].id;
const getElementIdFromBlockId = (block: TSurveyBlock): string => block.elements[0].id;
const getBlockName = (index: number): string => {
return `Block ${index + 1}`;
};
const handleQuestionLogicChange = (survey: TSurvey, compareId: string, updatedId: string): TSurvey => {
const handleElementLogicChange = (survey: TSurvey, compareId: string, updatedId: string): TSurvey => {
const updateConditions = (conditions: TConditionGroup): TConditionGroup => {
return {
...conditions,
@@ -205,38 +204,38 @@ export const QuestionsView = ({
};
useEffect(() => {
if (!invalidQuestions) return;
let updatedInvalidQuestions: string[] = invalidQuestions;
if (!invalidElements) return;
let updatedInvalidElements: string[] = invalidElements;
// Check welcome card
if (localSurvey.welcomeCard.enabled && !isWelcomeCardValid(localSurvey.welcomeCard, surveyLanguages)) {
if (!updatedInvalidQuestions.includes("start")) {
updatedInvalidQuestions.push("start");
if (!updatedInvalidElements.includes("start")) {
updatedInvalidElements.push("start");
}
} else {
updatedInvalidQuestions = updatedInvalidQuestions.filter((questionId) => questionId !== "start");
updatedInvalidElements = updatedInvalidElements.filter((elementId) => elementId !== "start");
}
// Check thank you card
localSurvey.endings.forEach((ending) => {
if (!isEndingCardValid(ending, surveyLanguages)) {
if (!updatedInvalidQuestions.includes(ending.id)) {
updatedInvalidQuestions.push(ending.id);
if (!updatedInvalidElements.includes(ending.id)) {
updatedInvalidElements.push(ending.id);
}
} else {
updatedInvalidQuestions = updatedInvalidQuestions.filter((questionId) => questionId !== ending.id);
updatedInvalidElements = updatedInvalidElements.filter((elementId) => elementId !== ending.id);
}
});
if (JSON.stringify(updatedInvalidQuestions) !== JSON.stringify(invalidQuestions)) {
setInvalidQuestions(updatedInvalidQuestions);
if (JSON.stringify(updatedInvalidElements) !== JSON.stringify(invalidElements)) {
setInvalidElements(updatedInvalidElements);
}
}, [localSurvey.welcomeCard, localSurvey.endings, surveyLanguages, invalidQuestions, setInvalidQuestions]);
}, [localSurvey.welcomeCard, localSurvey.endings, surveyLanguages, invalidElements, setInvalidElements]);
// function to validate individual elements
const validateSurveyElement = (element: TSurveyElement) => {
// prevent this function to execute further if user hasnt still tried to save the survey
if (invalidQuestions === null) {
if (invalidElements === null) {
return;
}
@@ -246,27 +245,27 @@ export const QuestionsView = ({
for (const blockId of blocksWithCyclicLogic) {
const block = localSurvey.blocks.find((b) => b.id === blockId);
if (block) {
const elementId = getQuestionIdFromBlockId(block);
const elementId = getElementIdFromBlockId(block);
if (elementId === element.id) {
setInvalidQuestions([...invalidQuestions, element.id]);
setInvalidElements([...invalidElements, element.id]);
return;
}
}
}
setInvalidQuestions(invalidQuestions.filter((id) => id !== element.id));
setInvalidElements(invalidElements.filter((id) => id !== element.id));
return;
}
setInvalidQuestions([...invalidQuestions, element.id]);
setInvalidElements([...invalidElements, element.id]);
return;
};
const updateQuestion = (questionIdx: number, updatedAttributes: any) => {
const question = questions[questionIdx];
if (!question) return;
const updateElement = (elementIdx: number, updatedAttributes: any) => {
const element = elements[elementIdx];
if (!element) return;
const { blockId, blockIndex } = findElementLocation(localSurvey, question.id);
const { blockId, blockIndex } = findElementLocation(localSurvey, element.id);
if (!blockId || blockIndex === -1) return;
let updatedSurvey = { ...localSurvey };
@@ -295,19 +294,19 @@ export const QuestionsView = ({
// Handle element ID changes
if ("id" in elementLevelAttributes) {
// if the survey question whose id is to be changed is linked to logic of any other survey then changing it
const initialQuestionId = question.id;
updatedSurvey = handleQuestionLogicChange(updatedSurvey, initialQuestionId, elementLevelAttributes.id);
if (invalidQuestions?.includes(initialQuestionId)) {
setInvalidQuestions(
invalidQuestions.map((id) => (id === initialQuestionId ? elementLevelAttributes.id : id))
// if the survey element whose id is to be changed is linked to logic of any other survey then changing it
const initialElementId = element.id;
updatedSurvey = handleElementLogicChange(updatedSurvey, initialElementId, elementLevelAttributes.id);
if (invalidElements?.includes(initialElementId)) {
setInvalidElements(
invalidElements.map((id) => (id === initialElementId ? elementLevelAttributes.id : id))
);
}
// relink the question to internal Id
internalQuestionIdMap[elementLevelAttributes.id] = internalQuestionIdMap[question.id];
delete internalQuestionIdMap[question.id];
setActiveQuestionId(elementLevelAttributes.id);
// relink the element to internal Id
internalElementIdMap[elementLevelAttributes.id] = internalElementIdMap[element.id];
delete internalElementIdMap[element.id];
setActiveElementId(elementLevelAttributes.id);
}
// Update element-level attributes if any
@@ -329,7 +328,7 @@ export const QuestionsView = ({
}
});
const result = updateElementInBlock(updatedSurvey, blockId, question.id, cleanedAttributes);
const result = updateElementInBlock(updatedSurvey, blockId, element.id, cleanedAttributes);
if (!result.ok) {
toast.error(result.error.message);
@@ -341,7 +340,7 @@ export const QuestionsView = ({
// Validate the updated element
const updatedElement = updatedSurvey.blocks
?.flatMap((b) => b.elements)
.find((q) => q.id === (cleanedAttributes.id ?? question.id));
.find((q) => q.id === (cleanedAttributes.id ?? element.id));
if (updatedElement) {
validateSurveyElement(updatedElement);
}
@@ -401,51 +400,53 @@ export const QuestionsView = ({
});
};
const deleteQuestion = (questionIdx: number) => {
const question = questions[questionIdx];
if (!question) return;
const deleteElement = (elementIdx: number) => {
const element = elements[elementIdx];
if (!element) return;
const questionId = question.id;
const activeQuestionIdTemp = activeQuestionId ?? questions[0]?.id;
const elementId = element.id;
const activeElementIdTemp = activeElementId ?? elements[0]?.id;
let updatedSurvey: TSurvey = { ...localSurvey };
// checking if this question is used in logic of any other question
const quesIdx = findQuestionUsedInLogic(localSurvey, questionId);
if (quesIdx !== -1) {
toast.error(t("environments.surveys.edit.question_used_in_logic", { questionIndex: quesIdx + 1 }));
return;
}
const recallQuestionIdx = isUsedInRecall(localSurvey, questionId);
if (recallQuestionIdx === questions.length) {
toast.error(t("environments.surveys.edit.question_used_in_recall_ending_card"));
return;
}
if (recallQuestionIdx !== -1) {
// checking if this element is used in logic of any other element
const usedElementIdx = findElementUsedInLogic(localSurvey, elementId);
if (usedElementIdx !== -1) {
toast.error(
t("environments.surveys.edit.question_used_in_recall", { questionIndex: recallQuestionIdx + 1 })
t("environments.surveys.edit.question_used_in_logic", { questionIndex: usedElementIdx + 1 })
);
return;
}
const quotaIdx = quotas.findIndex((quota) => isUsedInQuota(quota, { questionId }));
const recallElementIdx = isUsedInRecall(localSurvey, elementId);
if (recallElementIdx === elements.length) {
toast.error(t("environments.surveys.edit.question_used_in_recall_ending_card"));
return;
}
if (recallElementIdx !== -1) {
toast.error(
t("environments.surveys.edit.question_used_in_recall", { questionIndex: recallElementIdx + 1 })
);
return;
}
const quotaIdx = quotas.findIndex((quota) => isUsedInQuota(quota, { elementId: elementId }));
if (quotaIdx !== -1) {
toast.error(
t("environments.surveys.edit.question_used_in_quota", {
questionIndex: questionIdx + 1,
questionIndex: elementIdx + 1,
quotaName: quotas[quotaIdx].name,
})
);
return;
}
// check if we are recalling from this question for every language
// check if we are recalling from this element for every language
updatedSurvey.blocks = (updatedSurvey.blocks ?? []).map((block) => ({
...block,
elements: block.elements.map((element) => {
const updatedElement = { ...element };
for (const [languageCode, headline] of Object.entries(element.headline)) {
if (headline.includes(`recall:${questionId}`)) {
if (headline.includes(`recall:${elementId}`)) {
const recallInfo = extractRecallInfo(headline);
if (recallInfo) {
updatedElement.headline = {
@@ -459,8 +460,8 @@ export const QuestionsView = ({
}),
}));
// Find the block containing this question
const { blockId, blockIndex } = findElementLocation(localSurvey, questionId);
// Find the block containing this element
const { blockId, blockIndex } = findElementLocation(localSurvey, elementId);
if (!blockId || blockIndex === -1) return;
const block = updatedSurvey.blocks[blockIndex];
@@ -475,7 +476,7 @@ export const QuestionsView = ({
updatedSurvey = result.data;
} else {
// Otherwise, just remove this element from the block
const result = deleteElementFromBlock(updatedSurvey, blockId, questionId);
const result = deleteElementFromBlock(updatedSurvey, blockId, elementId);
if (!result.ok) {
toast.error(result.error.message);
return;
@@ -485,30 +486,30 @@ export const QuestionsView = ({
const firstEndingCard = localSurvey.endings[0];
setLocalSurvey(updatedSurvey);
delete internalQuestionIdMap[questionId];
delete internalElementIdMap[elementId];
if (questionId === activeQuestionIdTemp) {
const newQuestions = updatedSurvey.blocks.flatMap((b) => b.elements) ?? [];
if (questionIdx <= newQuestions.length && newQuestions.length > 0) {
setActiveQuestionId(newQuestions[questionIdx % newQuestions.length].id);
if (elementId === activeElementIdTemp) {
const newElements = updatedSurvey.blocks.flatMap((b) => b.elements) ?? [];
if (elementIdx <= newElements.length && newElements.length > 0) {
setActiveElementId(newElements[elementIdx % newElements.length].id);
} else if (firstEndingCard) {
setActiveQuestionId(firstEndingCard.id);
setActiveElementId(firstEndingCard.id);
}
}
toast.success(t("environments.surveys.edit.question_deleted"));
};
const duplicateQuestion = (questionIdx: number) => {
const question = questions[questionIdx];
if (!question) return;
const duplicateElement = (elementIdx: number) => {
const element = elements[elementIdx];
if (!element) return;
const { blockId, blockIndex } = findElementLocation(localSurvey, question.id);
const { blockId, blockIndex } = findElementLocation(localSurvey, element.id);
if (!blockId || blockIndex === -1) return;
// Create a duplicate of the element with a new ID
const newElementId = createId();
const duplicatedElement = { ...question, id: newElementId };
const duplicatedElement = { ...element, id: newElementId };
// Add the duplicated element to the same block
const result = addElementToBlock(localSurvey, blockId, duplicatedElement);
@@ -518,21 +519,21 @@ export const QuestionsView = ({
return;
}
setActiveQuestionId(newElementId);
internalQuestionIdMap[newElementId] = createId();
setActiveElementId(newElementId);
internalElementIdMap[newElementId] = createId();
setLocalSurvey(result.data);
toast.success(t("environments.surveys.edit.question_duplicated"));
};
const addQuestion = (question: TSurveyElement, index?: number) => {
const addElement = (element: TSurveyElement, index?: number) => {
const languageSymbols = extractLanguageCodes(localSurvey.languages);
const updatedQuestion = addMultiLanguageLabels(question, languageSymbols);
const updatedElement = addMultiLanguageLabels(element, languageSymbols);
const blockName = getBlockName(index ?? localSurvey.blocks.length);
const newBlock = {
name: blockName,
elements: [{ ...updatedQuestion, isDraft: true }],
elements: [{ ...updatedElement, isDraft: true }],
};
const result = addBlock(t, localSurvey, newBlock, index);
@@ -543,20 +544,20 @@ export const QuestionsView = ({
}
setLocalSurvey(result.data);
setActiveQuestionId(question.id);
internalQuestionIdMap[question.id] = createId();
setActiveElementId(element.id);
internalElementIdMap[element.id] = createId();
};
const _addElementToBlock = (question: TSurveyElement, blockId: string, afterElementIdx: number) => {
const _addElementToBlock = (element: TSurveyElement, blockId: string, afterElementIdx: number) => {
const languageSymbols = extractLanguageCodes(localSurvey.languages);
const updatedQuestion = addMultiLanguageLabels(question, languageSymbols);
const updatedElement = addMultiLanguageLabels(element, languageSymbols);
const targetIndex = afterElementIdx + 1;
const result = addElementToBlock(
localSurvey,
blockId,
{
...updatedQuestion,
...updatedElement,
isDraft: true,
},
targetIndex
@@ -568,8 +569,8 @@ export const QuestionsView = ({
}
setLocalSurvey(result.data);
setActiveQuestionId(updatedQuestion.id);
internalQuestionIdMap[updatedQuestion.id] = createId();
setActiveElementId(updatedElement.id);
internalElementIdMap[updatedElement.id] = createId();
};
const moveElementToBlock = (elementId: string, targetBlockId: string) => {
@@ -616,7 +617,7 @@ export const QuestionsView = ({
targetBlock.elements.push(elementToMove);
setLocalSurvey(updatedSurvey);
setActiveQuestionId(elementId);
setActiveElementId(elementId);
};
const addEndingCard = (index: number) => {
@@ -624,27 +625,27 @@ export const QuestionsView = ({
const newEndingCard = getDefaultEndingCard(localSurvey.languages, t);
updatedSurvey.endings.splice(index, 0, newEndingCard);
setActiveQuestionId(newEndingCard.id);
setActiveElementId(newEndingCard.id);
setLocalSurvey(updatedSurvey);
};
const moveQuestion = (questionIndex: number, up: boolean) => {
const question = questions[questionIndex];
if (!question) return;
const moveElement = (elementIdx: number, up: boolean) => {
const element = elements[elementIdx];
if (!element) return;
const { blockId, blockIndex } = findElementLocation(localSurvey, question.id);
const { blockId, blockIndex } = findElementLocation(localSurvey, element.id);
if (!blockId || blockIndex === -1) return;
const block = localSurvey.blocks[blockIndex];
const elementIndex = block.elements.findIndex((el) => el.id === question.id);
const elementIndex = block.elements.findIndex((el) => el.id === element.id);
// If block has multiple elements, move element within the block
if (block.elements.length > 1) {
// Check if we can move in the desired direction within the block
if ((up && elementIndex > 0) || (!up && elementIndex < block.elements.length - 1)) {
const direction = up ? "up" : "down";
const result = moveElementInBlock(localSurvey, blockId, question.id, direction);
const result = moveElementInBlock(localSurvey, blockId, element.id, direction);
if (!result.ok) {
toast.error(result.error.message);
@@ -683,8 +684,8 @@ export const QuestionsView = ({
if (blockIndex !== -1) {
const duplicatedBlock = result.data.blocks[blockIndex + 1];
if (duplicatedBlock?.elements[0]) {
setActiveQuestionId(duplicatedBlock.elements[0].id);
internalQuestionIdMap[duplicatedBlock.elements[0].id] = createId();
setActiveElementId(duplicatedBlock.elements[0].id);
internalElementIdMap[duplicatedBlock.elements[0].id] = createId();
}
}
@@ -700,12 +701,12 @@ export const QuestionsView = ({
return;
}
// Set active question to the first element of the first remaining block or ending card
// Set active element to the first element of the first remaining block or ending card
const newBlocks = result.data.blocks ?? [];
if (newBlocks.length > 0 && newBlocks[0].elements.length > 0) {
setActiveQuestionId(newBlocks[0].elements[0].id);
setActiveElementId(newBlocks[0].elements[0].id);
} else if (result.data.endings[0]) {
setActiveQuestionId(result.data.endings[0].id);
setActiveElementId(result.data.endings[0].id);
}
setLocalSurvey(result.data);
@@ -725,32 +726,32 @@ export const QuestionsView = ({
//useEffect to validate survey when changes are made to languages
useEffect(() => {
if (!invalidQuestions) return;
let updatedInvalidQuestions: string[] = invalidQuestions;
if (!invalidElements) return;
let updatedInvalidElements: string[] = invalidElements;
// Validate each element
questions.forEach((element) => {
updatedInvalidQuestions = validateSurveyElementsInBatch(
elements.forEach((element) => {
updatedInvalidElements = validateSurveyElementsInBatch(
element,
updatedInvalidQuestions,
updatedInvalidElements,
surveyLanguages
);
});
if (JSON.stringify(updatedInvalidQuestions) !== JSON.stringify(invalidQuestions)) {
setInvalidQuestions(updatedInvalidQuestions);
if (JSON.stringify(updatedInvalidElements) !== JSON.stringify(invalidElements)) {
setInvalidElements(updatedInvalidElements);
}
}, [questions, surveyLanguages, invalidQuestions, setInvalidQuestions]);
}, [elements, surveyLanguages, invalidElements, setInvalidElements]);
useEffect(() => {
const questionWithEmptyFallback = checkForEmptyFallBackValue(localSurvey, selectedLanguageCode);
if (questionWithEmptyFallback) {
setActiveQuestionId(questionWithEmptyFallback.id);
if (activeQuestionId === questionWithEmptyFallback.id) {
const elementWithEmptyFallback = checkForEmptyFallBackValue(localSurvey, selectedLanguageCode);
if (elementWithEmptyFallback) {
setActiveElementId(elementWithEmptyFallback.id);
if (activeElementId === elementWithEmptyFallback.id) {
toast.error(t("environments.surveys.edit.fallback_missing"));
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeQuestionId, setActiveQuestionId]);
}, [activeElementId, setActiveElementId]);
const sensors = useSensors(
useSensor(PointerSensor, {
@@ -765,7 +766,6 @@ export const QuestionsView = ({
if (!over) return;
// Check if we're dragging a block (not a question/element)
const sourceBlockIndex = localSurvey.blocks.findIndex((b) => b.id === active.id);
const destBlockIndex = localSurvey.blocks.findIndex((b) => b.id === over.id);
@@ -802,9 +802,9 @@ export const QuestionsView = ({
<EditWelcomeCard
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
setActiveQuestionId={setActiveQuestionId}
activeQuestionId={activeQuestionId}
isInvalid={invalidQuestions ? invalidQuestions.includes("start") : false}
setActiveElementId={setActiveElementId}
activeElementId={activeElementId}
isInvalid={invalidElements ? invalidElements.includes("start") : false}
setSelectedLanguageCode={setSelectedLanguageCode}
selectedLanguageCode={selectedLanguageCode}
locale={locale}
@@ -822,19 +822,19 @@ export const QuestionsView = ({
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
project={project}
moveQuestion={moveQuestion}
updateQuestion={updateQuestion}
moveElement={moveElement}
updateElement={updateElement}
updateBlockLogic={updateBlockLogic}
updateBlockLogicFallback={updateBlockLogicFallback}
updateBlockButtonLabel={updateBlockButtonLabel}
duplicateQuestion={duplicateQuestion}
duplicateElement={duplicateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
deleteQuestion={deleteQuestion}
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}
invalidQuestions={invalidQuestions}
addQuestion={addQuestion}
deleteElement={deleteElement}
activeElementId={activeElementId}
setActiveElementId={setActiveElementId}
invalidElements={invalidElements}
addElement={addElement}
isFormbricksCloud={isFormbricksCloud}
isCxMode={isCxMode}
locale={locale}
@@ -850,7 +850,7 @@ export const QuestionsView = ({
/>
</DndContext>
<AddQuestionButton addQuestion={addQuestion} project={project} isCxMode={isCxMode} />
<AddElementButton addElement={addElement} project={project} isCxMode={isCxMode} />
<div className="mt-5 flex flex-col gap-5" ref={parent}>
<hr className="border-t border-dashed" />
<DndContext
@@ -866,9 +866,9 @@ export const QuestionsView = ({
localSurvey={localSurvey}
endingCardIndex={index}
setLocalSurvey={setLocalSurvey}
setActiveQuestionId={setActiveQuestionId}
activeQuestionId={activeQuestionId}
isInvalid={invalidQuestions ? invalidQuestions.includes(ending.id) : false}
setActiveElementId={setActiveElementId}
activeElementId={activeElementId}
isInvalid={invalidElements ? invalidElements.includes(ending.id) : false}
setSelectedLanguageCode={setSelectedLanguageCode}
selectedLanguageCode={selectedLanguageCode}
addEndingCard={addEndingCard}
@@ -891,16 +891,16 @@ export const QuestionsView = ({
<HiddenFieldsCard
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
setActiveQuestionId={setActiveQuestionId}
activeQuestionId={activeQuestionId}
setActiveElementId={setActiveElementId}
activeElementId={activeElementId}
quotas={quotas}
/>
<SurveyVariablesCard
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}
activeElementId={activeElementId}
setActiveElementId={setActiveElementId}
quotas={quotas}
/>
@@ -908,8 +908,8 @@ export const QuestionsView = ({
localSurvey={localSurvey}
projectLanguages={projectLanguages}
setLocalSurvey={setLocalSurvey}
setActiveQuestionId={setActiveQuestionId}
activeQuestionId={activeQuestionId}
setActiveElementId={setActiveElementId}
activeElementId={activeElementId}
isMultiLanguageAllowed={isMultiLanguageAllowed}
isFormbricksCloud={isFormbricksCloud}
setSelectedLanguageCode={setSelectedLanguageCode}

View File

@@ -7,8 +7,8 @@ import { TSurvey, TSurveyEndScreenCard } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes, getLocalizedValue } from "@/lib/i18n/utils";
import { headlineToRecall, recallToHeadline } from "@/lib/utils/recall";
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { RecallWrapper } from "@/modules/survey/components/question-form-input/components/recall-wrapper";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { RecallWrapper } from "@/modules/survey/components/element-form-input/components/recall-wrapper";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
@@ -53,12 +53,12 @@ export const EndScreenForm = ({
return (
<form>
<QuestionFormInput
<ElementFormInput
id="headline"
label={t("common.note") + "*"}
value={endingCard.headline}
localSurvey={localSurvey}
questionIdx={questions.length + endingCardIndex}
elementIdx={questions.length + endingCardIndex}
isInvalid={isInvalid}
updateSurvey={updateSurvey}
selectedLanguageCode={selectedLanguageCode}
@@ -71,12 +71,12 @@ export const EndScreenForm = ({
{endingCard.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
<ElementFormInput
id="subheader"
value={endingCard.subheader}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questions.length + endingCardIndex}
elementIdx={questions.length + endingCardIndex}
isInvalid={isInvalid}
updateSurvey={updateSurvey}
selectedLanguageCode={selectedLanguageCode}
@@ -138,14 +138,14 @@ export const EndScreenForm = ({
{showEndingCardCTA && (
<div className="border-1 mt-4 space-y-4 rounded-md border bg-slate-100 p-4 pt-2">
<div className="space-y-2">
<QuestionFormInput
<ElementFormInput
id="buttonLabel"
label={t("environments.surveys.edit.button_label")}
placeholder={t("environments.surveys.edit.create_your_own_survey")}
className="rounded-md"
value={endingCard.buttonLabel}
localSurvey={localSurvey}
questionIdx={questions.length + endingCardIndex}
elementIdx={questions.length + endingCardIndex}
isInvalid={isInvalid}
updateSurvey={updateSurvey}
selectedLanguageCode={selectedLanguageCode}
@@ -159,7 +159,7 @@ export const EndScreenForm = ({
<div className="rounded-md bg-white">
<RecallWrapper
value={endingCard.buttonLink ?? ""}
questionId={endingCard.id}
elementId={endingCard.id}
onChange={(val, recallItems, fallbacks) => {
const updatedValue = {
...endingCard,

View File

@@ -12,7 +12,7 @@ import { TSurveyFileUploadElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
@@ -21,9 +21,9 @@ import { useGetBillingInfo } from "@/modules/utils/hooks/useGetBillingInfo";
interface FileUploadFormProps {
localSurvey: TSurvey;
project?: Project;
question: TSurveyFileUploadElement;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyFileUploadElement>) => void;
element: TSurveyFileUploadElement;
elementIdx: number;
updateElement: (elementIdx: number, updatedAttributes: Partial<TSurveyFileUploadElement>) => void;
selectedLanguageCode: string;
setSelectedLanguageCode: (languageCode: string) => void;
isInvalid: boolean;
@@ -33,11 +33,11 @@ interface FileUploadFormProps {
isExternalUrlsAllowed?: boolean;
}
export const FileUploadQuestionForm = ({
export const FileUploadElementForm = ({
localSurvey,
question,
questionIdx,
updateQuestion,
element,
elementIdx,
updateElement,
isInvalid,
project,
selectedLanguageCode,
@@ -88,11 +88,11 @@ export const FileUploadQuestionForm = ({
return;
}
const currentExtensions = question.allowedFileExtensions || [];
const currentExtensions = element.allowedFileExtensions || [];
// Check if the lowercase extension already exists
if (!currentExtensions.includes(modifiedExtension)) {
updateQuestion(questionIdx, {
updateElement(elementIdx, {
allowedFileExtensions: [...currentExtensions, modifiedExtension],
});
setExtension(""); // Clear the input field
@@ -103,11 +103,11 @@ export const FileUploadQuestionForm = ({
const removeExtension = (event, index: number) => {
event.preventDefault();
if (question.allowedFileExtensions) {
const updatedExtensions = [...(question.allowedFileExtensions || [])];
if (element.allowedFileExtensions) {
const updatedExtensions = [...(element.allowedFileExtensions || [])];
updatedExtensions.splice(index, 1);
// Ensure array is set to undefined if empty, matching toggle behavior
updateQuestion(questionIdx, {
updateElement(elementIdx, {
allowedFileExtensions: updatedExtensions.length > 0 ? updatedExtensions : undefined,
});
}
@@ -129,58 +129,58 @@ export const FileUploadQuestionForm = ({
const handleMaxSizeInMBToggle = (checked: boolean) => {
const defaultMaxSizeInMB = isFormbricksCloud ? maxSizeInMBLimit : 1024;
updateQuestion(questionIdx, { maxSizeInMB: checked ? defaultMaxSizeInMB : undefined });
updateElement(elementIdx, { maxSizeInMB: checked ? defaultMaxSizeInMB : undefined });
};
const [parent] = useAutoAnimate();
return (
<form>
<QuestionFormInput
<ElementFormInput
id="headline"
value={question.headline}
value={element.headline}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.headline?.default || question.headline.default.trim() === ""}
autoFocus={!element.headline?.default || element.headline.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
<div ref={parent}>
{question.subheader !== undefined && (
{element.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
<ElementFormInput
id="subheader"
value={question.subheader}
value={element.subheader}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.subheader?.default || question.subheader.default.trim() === ""}
autoFocus={!element.subheader?.default || element.subheader.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
</div>
</div>
)}
{question.subheader === undefined && (
{element.subheader === undefined && (
<Button
size="sm"
className="mt-3"
variant="secondary"
type="button"
onClick={() => {
updateQuestion(questionIdx, {
updateElement(elementIdx, {
subheader: createI18nString("", surveyLanguageCodes),
});
}}>
@@ -191,8 +191,8 @@ export const FileUploadQuestionForm = ({
</div>
<div className="mt-6 space-y-6">
<AdvancedOptionToggle
isChecked={question.allowMultipleFiles}
onToggle={() => updateQuestion(questionIdx, { allowMultipleFiles: !question.allowMultipleFiles })}
isChecked={element.allowMultipleFiles}
onToggle={() => updateElement(elementIdx, { allowMultipleFiles: !element.allowMultipleFiles })}
htmlId="allowMultipleFile"
title={t("environments.surveys.edit.allow_multiple_files")}
description={t("environments.surveys.edit.let_people_upload_up_to_25_files_at_the_same_time")}
@@ -200,7 +200,7 @@ export const FileUploadQuestionForm = ({
customContainerClass="p-0"></AdvancedOptionToggle>
<AdvancedOptionToggle
isChecked={!!question.maxSizeInMB}
isChecked={!!element.maxSizeInMB}
onToggle={handleMaxSizeInMBToggle}
htmlId="maxFileSize"
title={t("environments.surveys.edit.max_file_size")}
@@ -214,7 +214,7 @@ export const FileUploadQuestionForm = ({
autoFocus
type="number"
id="fileSizeLimit"
value={question.maxSizeInMB}
value={element.maxSizeInMB}
onChange={(e) => {
const parsedValue = parseInt(e.target.value, 10);
@@ -223,11 +223,11 @@ export const FileUploadQuestionForm = ({
`${t("environments.surveys.edit.max_file_size_limit_is")} ${maxSizeInMBLimit} MB`
);
setMaxSizeError(true);
updateQuestion(questionIdx, { maxSizeInMB: maxSizeInMBLimit });
updateElement(elementIdx, { maxSizeInMB: maxSizeInMBLimit });
return;
}
updateQuestion(questionIdx, { maxSizeInMB: parseInt(e.target.value, 10) });
updateElement(elementIdx, { maxSizeInMB: parseInt(e.target.value, 10) });
}}
className="ml-2 mr-2 inline w-20 bg-white text-center text-sm"
/>
@@ -249,9 +249,9 @@ export const FileUploadQuestionForm = ({
</AdvancedOptionToggle>
<AdvancedOptionToggle
isChecked={!!question.allowedFileExtensions}
isChecked={!!element.allowedFileExtensions}
onToggle={(checked) =>
updateQuestion(questionIdx, { allowedFileExtensions: checked ? [] : undefined })
updateElement(elementIdx, { allowedFileExtensions: checked ? [] : undefined })
}
htmlId="limitFileType"
title={t("environments.surveys.edit.limit_file_types")}
@@ -260,7 +260,7 @@ export const FileUploadQuestionForm = ({
customContainerClass="p-0">
<div className="p-4">
<div className="flex flex-row flex-wrap gap-2">
{question.allowedFileExtensions?.map((item, index) => (
{element.allowedFileExtensions?.map((item, index) => (
<div
key={item}
className="mb-2 flex h-8 items-center space-x-2 rounded-full bg-slate-200 px-2">

View File

@@ -7,7 +7,7 @@ import { useMemo, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TSurveyQuota } from "@formbricks/types/quota";
import { TSurvey, TSurveyHiddenFields, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { TSurvey, TSurveyHiddenFields } from "@formbricks/types/surveys/types";
import { validateId } from "@formbricks/types/surveys/validation";
import { cn } from "@/lib/cn";
import { extractRecallInfo } from "@/lib/utils/recall";
@@ -21,36 +21,35 @@ import { Tag } from "@/modules/ui/components/tag";
interface HiddenFieldsCardProps {
localSurvey: TSurvey;
setLocalSurvey: (survey: TSurvey) => void;
activeQuestionId: TSurveyQuestionId | null;
setActiveQuestionId: (questionId: TSurveyQuestionId | null) => void;
activeElementId: string | null;
setActiveElementId: (elementId: string | null) => void;
quotas: TSurveyQuota[];
}
export const HiddenFieldsCard = ({
activeQuestionId,
activeElementId,
localSurvey,
setActiveQuestionId,
setActiveElementId,
setLocalSurvey,
quotas,
}: HiddenFieldsCardProps) => {
const open = activeQuestionId == "hidden";
const open = activeElementId == "hidden";
const [hiddenField, setHiddenField] = useState<string>("");
const { t } = useTranslation();
const setOpen = (open: boolean) => {
if (open) {
// NOSONAR typescript:S2301 // the function usage is clear
setActiveQuestionId("hidden");
setActiveElementId("hidden");
} else {
setActiveQuestionId(null);
setActiveElementId(null);
}
};
const questions = useMemo(() => getElementsFromBlocks(localSurvey.blocks), [localSurvey.blocks]);
const elements = useMemo(() => getElementsFromBlocks(localSurvey.blocks), [localSurvey.blocks]);
const updateSurvey = (data: TSurveyHiddenFields, currentFieldId?: string) => {
let updatedSurvey = { ...localSurvey };
// Remove recall info from question/element headlines
if (currentFieldId) {
updatedSurvey.blocks = updatedSurvey.blocks.map((block) => ({
...block,
@@ -93,26 +92,27 @@ export const HiddenFieldsCard = ({
);
return;
}
const recallQuestionIdx = isUsedInRecall(localSurvey, fieldId);
if (recallQuestionIdx === -2) {
const recallElementIdx = isUsedInRecall(localSurvey, fieldId);
if (recallElementIdx === -2) {
toast.error(
t("environments.surveys.edit.hidden_field_used_in_recall_welcome", { hiddenField: fieldId })
);
return;
}
const totalQuestions = questions.length;
if (recallQuestionIdx === totalQuestions) {
const totalElements = elements.length;
if (recallElementIdx === totalElements) {
toast.error(
t("environments.surveys.edit.hidden_field_used_in_recall_ending_card", { hiddenField: fieldId })
);
return;
}
if (recallQuestionIdx !== -1) {
if (recallElementIdx !== -1) {
toast.error(
t("environments.surveys.edit.hidden_field_used_in_recall", {
hiddenField: fieldId,
questionIndex: recallQuestionIdx + 1,
questionIndex: recallElementIdx + 1,
})
);
return;
@@ -200,13 +200,13 @@ export const HiddenFieldsCard = ({
className="mt-5"
onSubmit={(e) => {
e.preventDefault();
const existingQuestionIds = questions.map((question) => question.id);
const existingElementIds = elements.map((element) => element.id);
const existingEndingCardIds = localSurvey.endings.map((ending) => ending.id);
const existingHiddenFieldIds = localSurvey.hiddenFields.fieldIds ?? [];
const validateIdError = validateId(
"Hidden field",
hiddenField,
existingQuestionIds,
existingElementIds,
existingEndingCardIds,
existingHiddenFieldIds
);

View File

@@ -13,7 +13,7 @@ import { TSurveyMatrixElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { MatrixSortableItem } from "@/modules/survey/editor/components/matrix-sortable-item";
import { findOptionUsedInLogic } from "@/modules/survey/editor/lib/utils";
import { Button } from "@/modules/ui/components/button";
@@ -21,11 +21,11 @@ import { Label } from "@/modules/ui/components/label";
import { ShuffleOptionSelect } from "@/modules/ui/components/shuffle-option-select";
import { isLabelValidForAllLanguages } from "../lib/validation";
interface MatrixQuestionFormProps {
interface MatrixElementFormProps {
localSurvey: TSurvey;
question: TSurveyMatrixElement;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyMatrixElement>) => void;
element: TSurveyMatrixElement;
elementIdx: number;
updateElement: (elementIdx: number, updatedAttributes: Partial<TSurveyMatrixElement>) => void;
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
@@ -34,10 +34,10 @@ interface MatrixQuestionFormProps {
isExternalUrlsAllowed?: boolean;
}
export const MatrixQuestionForm = ({
question,
questionIdx,
updateQuestion,
export const MatrixElementForm = ({
element,
elementIdx,
updateElement,
isInvalid,
localSurvey,
selectedLanguageCode,
@@ -45,7 +45,7 @@ export const MatrixQuestionForm = ({
locale,
isStorageConfigured = true,
isExternalUrlsAllowed,
}: MatrixQuestionFormProps): JSX.Element => {
}: MatrixElementFormProps): JSX.Element => {
const languageCodes = extractLanguageCodes(localSurvey.languages);
const { t } = useTranslation();
@@ -57,41 +57,41 @@ export const MatrixQuestionForm = ({
// Function to add a new Label input field
const handleAddLabel = (type: "row" | "column") => {
if (type === "row") {
const updatedRows = [...question.rows, { id: createId(), label: createI18nString("", languageCodes) }];
updateQuestion(questionIdx, { rows: updatedRows });
const updatedRows = [...element.rows, { id: createId(), label: createI18nString("", languageCodes) }];
updateElement(elementIdx, { rows: updatedRows });
setTimeout(() => focusItem(updatedRows.length - 1, type), 0);
} else {
const updatedColumns = [
...question.columns,
...element.columns,
{ id: createId(), label: createI18nString("", languageCodes) },
];
updateQuestion(questionIdx, { columns: updatedColumns });
updateElement(elementIdx, { columns: updatedColumns });
setTimeout(() => focusItem(updatedColumns.length - 1, type), 0);
}
};
// Function to delete a label input field
const handleDeleteLabel = (type: "row" | "column", index: number) => {
const labels = type === "row" ? question.rows : question.columns;
const labels = type === "row" ? element.rows : element.columns;
if (labels.length <= 2) return; // Prevent deleting below minimum length
// check if the label is used in logic
if (type === "column") {
const questionIdx = findOptionUsedInLogic(localSurvey, question.id, index.toString());
if (questionIdx !== -1) {
const elementIdx = findOptionUsedInLogic(localSurvey, element.id, index.toString());
if (elementIdx !== -1) {
toast.error(
t("environments.surveys.edit.column_used_in_logic_error", {
questionIndex: questionIdx + 1,
questionIndex: elementIdx + 1,
})
);
return;
}
} else {
const questionIdx = findOptionUsedInLogic(localSurvey, question.id, index.toString(), true);
if (questionIdx !== -1) {
const elementIdx = findOptionUsedInLogic(localSurvey, element.id, index.toString(), true);
if (elementIdx !== -1) {
toast.error(
t("environments.surveys.edit.row_used_in_logic_error", {
questionIndex: questionIdx + 1,
questionIndex: elementIdx + 1,
})
);
return;
@@ -101,14 +101,14 @@ export const MatrixQuestionForm = ({
const updatedLabels = labels.filter((_, idx) => idx !== index);
if (type === "row") {
updateQuestion(questionIdx, { rows: updatedLabels });
updateElement(elementIdx, { rows: updatedLabels });
} else {
updateQuestion(questionIdx, { columns: updatedLabels });
updateElement(elementIdx, { columns: updatedLabels });
}
};
const updateMatrixLabel = (index: number, type: "row" | "column", matrixLabel: TI18nString) => {
const labels = type === "row" ? [...question.rows] : [...question.columns];
const labels = type === "row" ? [...element.rows] : [...element.columns];
// Update the label at the given index, or add a new label if index is undefined
if (index !== undefined) {
@@ -117,14 +117,14 @@ export const MatrixQuestionForm = ({
labels.push({ id: createId(), label: matrixLabel });
}
if (type === "row") {
updateQuestion(questionIdx, { rows: labels });
updateElement(elementIdx, { rows: labels });
} else {
updateQuestion(questionIdx, { columns: labels });
updateElement(elementIdx, { columns: labels });
}
};
const handleKeyDown = (e: React.KeyboardEvent, type: "row" | "column", currentIndex: number) => {
const items = type === "row" ? question.rows : question.columns;
const items = type === "row" ? element.rows : element.columns;
if (e.key === "Enter") {
e.preventDefault();
@@ -156,7 +156,7 @@ export const MatrixQuestionForm = ({
if (!active || !over || active.id === over.id) return;
const items = type === "row" ? [...question.rows] : [...question.columns];
const items = type === "row" ? [...element.rows] : [...element.columns];
const activeIndex = items.findIndex((item) => item.id === active.id);
const overIndex = items.findIndex((item) => item.id === over.id);
@@ -166,9 +166,9 @@ export const MatrixQuestionForm = ({
items.splice(activeIndex, 1);
items.splice(overIndex, 0, movedItem);
updateQuestion(questionIdx, type === "row" ? { rows: items } : { columns: items });
updateElement(elementIdx, type === "row" ? { rows: items } : { columns: items });
},
[questionIdx, updateQuestion, question.rows, question.columns]
[elementIdx, updateElement, element.rows, element.columns]
);
const shuffleOptionsTypes = {
@@ -192,51 +192,51 @@ export const MatrixQuestionForm = ({
return (
<form>
<QuestionFormInput
<ElementFormInput
id="headline"
value={question.headline}
value={element.headline}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.headline?.default || question.headline.default.trim() === ""}
autoFocus={!element.headline?.default || element.headline.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
<div ref={parent}>
{question.subheader !== undefined && (
{element.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
<ElementFormInput
id="subheader"
value={question.subheader}
value={element.subheader}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.subheader?.default || question.subheader.default.trim() === ""}
autoFocus={!element.subheader?.default || element.subheader.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
</div>
</div>
)}
{question.subheader === undefined && (
{element.subheader === undefined && (
<Button
size="sm"
variant="secondary"
className="mt-3"
type="button"
onClick={() => {
updateQuestion(questionIdx, {
updateElement(elementIdx, {
subheader: createI18nString("", languageCodes),
});
}}>
@@ -251,26 +251,26 @@ export const MatrixQuestionForm = ({
<Label htmlFor="rows">{t("environments.surveys.edit.rows")}</Label>
<div className="mt-2">
<DndContext id="matrix-rows" onDragEnd={(e) => handleMatrixDragEnd("row", e)}>
<SortableContext items={question.rows} strategy={verticalListSortingStrategy}>
<SortableContext items={element.rows} strategy={verticalListSortingStrategy}>
<div className="flex flex-col gap-2" ref={parent}>
{question.rows.map((row, index) => (
{element.rows.map((row, index) => (
<MatrixSortableItem
key={row.id}
choice={row}
index={index}
type="row"
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
element={element}
elementIdx={elementIdx}
updateMatrixLabel={updateMatrixLabel}
onDelete={(index) => handleDeleteLabel("row", index)}
onKeyDown={(e) => handleKeyDown(e, "row", index)}
canDelete={question.rows.length > 2}
canDelete={element.rows.length > 2}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={
isInvalid &&
!isLabelValidForAllLanguages(question.rows[index].label, localSurvey.languages)
!isLabelValidForAllLanguages(element.rows[index].label, localSurvey.languages)
}
locale={locale}
isStorageConfigured={isStorageConfigured}
@@ -297,26 +297,26 @@ export const MatrixQuestionForm = ({
<Label htmlFor="columns">{t("environments.surveys.edit.columns")}</Label>
<div className="mt-2">
<DndContext id="matrix-columns" onDragEnd={(e) => handleMatrixDragEnd("column", e)}>
<SortableContext items={question.columns} strategy={verticalListSortingStrategy}>
<SortableContext items={element.columns} strategy={verticalListSortingStrategy}>
<div className="flex flex-col gap-2" ref={parent}>
{question.columns.map((column, index) => (
{element.columns.map((column, index) => (
<MatrixSortableItem
key={column.id}
choice={column}
index={index}
type="column"
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
element={element}
elementIdx={elementIdx}
updateMatrixLabel={updateMatrixLabel}
onDelete={(index) => handleDeleteLabel("column", index)}
onKeyDown={(e) => handleKeyDown(e, "column", index)}
canDelete={question.columns.length > 2}
canDelete={element.columns.length > 2}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={
isInvalid &&
!isLabelValidForAllLanguages(question.columns[index].label, localSurvey.languages)
!isLabelValidForAllLanguages(element.columns[index].label, localSurvey.languages)
}
locale={locale}
isStorageConfigured={isStorageConfigured}
@@ -340,9 +340,9 @@ export const MatrixQuestionForm = ({
<div className="mt-3 flex flex-1 items-center justify-end gap-2">
<ShuffleOptionSelect
shuffleOptionsTypes={shuffleOptionsTypes}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
shuffleOption={question.shuffleOption}
elementIdx={elementIdx}
updateElement={updateElement}
shuffleOption={element.shuffleOption}
/>
</div>
</div>

View File

@@ -9,7 +9,7 @@ import { type TI18nString } from "@formbricks/types/i18n";
import { TSurveyMatrixElement, TSurveyMatrixElementChoice } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { Button } from "@/modules/ui/components/button";
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
@@ -18,8 +18,8 @@ interface MatrixSortableItemProps {
type: "row" | "column";
index: number;
localSurvey: TSurvey;
question: TSurveyMatrixElement;
questionIdx: number;
element: TSurveyMatrixElement;
elementIdx: number;
updateMatrixLabel: (index: number, type: "row" | "column", matrixLabel: TI18nString) => void;
onDelete: (index: number) => void;
onKeyDown: (e: React.KeyboardEvent) => void;
@@ -36,7 +36,7 @@ export const MatrixSortableItem = ({
type,
index,
localSurvey,
questionIdx,
elementIdx,
updateMatrixLabel,
onDelete,
onKeyDown,
@@ -65,12 +65,12 @@ export const MatrixSortableItem = ({
</div>
<div className="flex w-full items-center">
<QuestionFormInput
<ElementFormInput
key={choice.id}
id={`${type}-${index}`}
label=""
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
value={choice.label}
updateMatrixLabel={updateMatrixLabel}
selectedLanguageCode={selectedLanguageCode}

View File

@@ -13,18 +13,18 @@ import { TSurveyElementTypeEnum, TSurveyMultipleChoiceElement } from "@formbrick
import { TShuffleOption, TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { QuestionOptionChoice } from "@/modules/survey/editor/components/question-option-choice";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { ElementOptionChoice } from "@/modules/survey/editor/components/element-option-choice";
import { findOptionUsedInLogic } from "@/modules/survey/editor/lib/utils";
import { Button } from "@/modules/ui/components/button";
import { Label } from "@/modules/ui/components/label";
import { ShuffleOptionSelect } from "@/modules/ui/components/shuffle-option-select";
interface MultipleChoiceQuestionFormProps {
interface MultipleChoiceElementFormProps {
localSurvey: TSurvey;
question: TSurveyMultipleChoiceElement;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyMultipleChoiceElement>) => void;
element: TSurveyMultipleChoiceElement;
elementIdx: number;
updateElement: (elementIdx: number, updatedAttributes: Partial<TSurveyMultipleChoiceElement>) => void;
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
@@ -33,10 +33,10 @@ interface MultipleChoiceQuestionFormProps {
isExternalUrlsAllowed?: boolean;
}
export const MultipleChoiceQuestionForm = ({
question,
questionIdx,
updateQuestion,
export const MultipleChoiceElementForm = ({
element,
elementIdx,
updateElement,
isInvalid,
localSurvey,
selectedLanguageCode,
@@ -44,13 +44,13 @@ export const MultipleChoiceQuestionForm = ({
locale,
isStorageConfigured = true,
isExternalUrlsAllowed,
}: MultipleChoiceQuestionFormProps): JSX.Element => {
}: MultipleChoiceElementFormProps): JSX.Element => {
const { t } = useTranslation();
const lastChoiceRef = useRef<HTMLInputElement>(null);
const [isNew, setIsNew] = useState(true);
const [isInvalidValue, setisInvalidValue] = useState<string | null>(null);
const questionRef = useRef<HTMLInputElement>(null);
const elementRef = useRef<HTMLInputElement>(null);
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const surveyLanguages = localSurvey.languages ?? [];
const shuffleOptionsTypes = {
@@ -62,7 +62,7 @@ export const MultipleChoiceQuestionForm = ({
all: {
id: "all",
label: t("environments.surveys.edit.randomize_all"),
show: question.choices.every((c) => c.id !== "other" && c.id !== "none"),
show: element.choices.every((c) => c.id !== "other" && c.id !== "none"),
},
exceptLast: {
id: "exceptLast",
@@ -73,21 +73,21 @@ export const MultipleChoiceQuestionForm = ({
const updateChoice = (choiceIdx: number, updatedAttributes: { label: TI18nString }) => {
let newChoices: any[] = [];
if (question.choices) {
newChoices = question.choices.map((choice, idx) => {
if (element.choices) {
newChoices = element.choices.map((choice, idx) => {
if (idx !== choiceIdx) return choice;
return { ...choice, ...updatedAttributes };
});
}
updateQuestion(questionIdx, {
updateElement(elementIdx, {
choices: newChoices,
});
};
const regularChoices = useMemo(
() => question.choices?.filter((c) => c.id !== "other" && c.id !== "none"),
[question.choices]
() => element.choices?.filter((c) => c.id !== "other" && c.id !== "none"),
[element.choices]
);
const ensureSpecialChoicesOrder = (choices: TSurveyMultipleChoiceElement["choices"]) => {
@@ -113,52 +113,52 @@ export const MultipleChoiceQuestionForm = ({
const newChoices = ensureSpecialChoicesOrder([
...regularChoices,
...question.choices.filter((c) => c.id === "other" || c.id === "none"),
...element.choices.filter((c) => c.id === "other" || c.id === "none"),
]);
updateQuestion(questionIdx, { choices: newChoices });
updateElement(elementIdx, { choices: newChoices });
};
const addSpecialChoice = (choiceId: "other" | "none", labelText: string) => {
if (question.choices.some((c) => c.id === choiceId)) return;
if (element.choices.some((c) => c.id === choiceId)) return;
const newChoice = {
id: choiceId,
label: createI18nString(labelText, surveyLanguageCodes),
};
const newChoices = ensureSpecialChoicesOrder([...question.choices, newChoice]);
const newChoices = ensureSpecialChoicesOrder([...element.choices, newChoice]);
updateQuestion(questionIdx, {
updateElement(elementIdx, {
choices: newChoices,
...(question.shuffleOption === shuffleOptionsTypes.all.id && {
...(element.shuffleOption === shuffleOptionsTypes.all.id && {
shuffleOption: shuffleOptionsTypes.exceptLast.id as TShuffleOption,
}),
});
};
const deleteChoice = (choiceIdx: number) => {
const choiceToDelete = question.choices[choiceIdx].id;
const choiceToDelete = element.choices[choiceIdx].id;
if (choiceToDelete !== "other" && choiceToDelete !== "none") {
const questionIdx = findOptionUsedInLogic(localSurvey, question.id, choiceToDelete);
if (questionIdx !== -1) {
const idx = findOptionUsedInLogic(localSurvey, element.id, choiceToDelete);
if (elementIdx !== -1) {
toast.error(
t("environments.surveys.edit.option_used_in_logic_error", {
questionIndex: questionIdx + 1,
questionIndex: idx + 1,
})
);
return;
}
}
const newChoices = !question.choices ? [] : question.choices.filter((_, idx) => idx !== choiceIdx);
const choiceValue = question.choices[choiceIdx].label[selectedLanguageCode];
const newChoices = !element.choices ? [] : element.choices.filter((_, idx) => idx !== choiceIdx);
const choiceValue = element.choices[choiceIdx].label[selectedLanguageCode];
if (isInvalidValue === choiceValue) {
setisInvalidValue(null);
}
updateQuestion(questionIdx, {
updateElement(elementIdx, {
choices: newChoices,
});
};
@@ -167,12 +167,12 @@ export const MultipleChoiceQuestionForm = ({
if (lastChoiceRef.current) {
lastChoiceRef.current?.focus();
}
}, [question.choices?.length]);
}, [element.choices?.length]);
// This effect will run once on initial render, setting focus to the question input.
// This effect will run once on initial render, setting focus to the element input.
useEffect(() => {
if (isNew && questionRef.current) {
questionRef.current.focus();
if (isNew && elementRef.current) {
elementRef.current.focus();
}
}, [isNew]);
@@ -196,52 +196,52 @@ export const MultipleChoiceQuestionForm = ({
return (
<form>
<QuestionFormInput
<ElementFormInput
id="headline"
value={question.headline}
value={element.headline}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.headline?.default || question.headline.default.trim() === ""}
autoFocus={!element.headline?.default || element.headline.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
<div ref={parent}>
{question.subheader !== undefined && (
{element.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
<ElementFormInput
id="subheader"
value={question.subheader}
value={element.subheader}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.subheader?.default || question.subheader.default.trim() === ""}
autoFocus={!element.subheader?.default || element.subheader.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
</div>
</div>
)}
{question.subheader === undefined && (
{element.subheader === undefined && (
<Button
size="sm"
variant="secondary"
className="mt-3"
type="button"
onClick={() => {
updateQuestion(questionIdx, {
updateElement(elementIdx, {
subheader: createI18nString("", surveyLanguageCodes),
});
}}>
@@ -272,24 +272,24 @@ export const MultipleChoiceQuestionForm = ({
return;
}
const activeIndex = question.choices.findIndex((choice) => choice.id === active.id);
const overIndex = question.choices.findIndex((choice) => choice.id === over.id);
const activeIndex = element.choices.findIndex((choice) => choice.id === active.id);
const overIndex = element.choices.findIndex((choice) => choice.id === over.id);
const newChoices = [...question.choices];
const newChoices = [...element.choices];
newChoices.splice(activeIndex, 1);
newChoices.splice(overIndex, 0, question.choices[activeIndex]);
newChoices.splice(overIndex, 0, element.choices[activeIndex]);
updateQuestion(questionIdx, { choices: newChoices });
updateElement(elementIdx, { choices: newChoices });
}}>
<SortableContext items={question.choices} strategy={verticalListSortingStrategy}>
<SortableContext items={element.choices} strategy={verticalListSortingStrategy}>
<div className="flex flex-col gap-2" ref={parent}>
{question.choices?.map((choice, choiceIdx) => (
<QuestionOptionChoice
{element.choices?.map((choice, choiceIdx) => (
<ElementOptionChoice
key={choice.id}
choice={choice}
choiceIdx={choiceIdx}
questionIdx={questionIdx}
elementIdx={elementIdx}
updateChoice={updateChoice}
deleteChoice={deleteChoice}
addChoice={addChoice}
@@ -298,8 +298,8 @@ export const MultipleChoiceQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
surveyLanguages={surveyLanguages}
question={question}
updateQuestion={updateQuestion}
element={element}
updateElement={updateElement}
surveyLanguageCodes={surveyLanguageCodes}
locale={locale}
isStorageConfigured={isStorageConfigured}
@@ -311,7 +311,7 @@ export const MultipleChoiceQuestionForm = ({
<div className="mt-2 flex items-center justify-between space-x-2">
<div className="flex gap-2">
{specialChoices.map((specialChoice) => {
if (question.choices.some((c) => c.id === specialChoice.id)) return null;
if (element.choices.some((c) => c.id === specialChoice.id)) return null;
return (
<Button
size="sm"
@@ -329,23 +329,23 @@ export const MultipleChoiceQuestionForm = ({
variant="secondary"
type="button"
onClick={() => {
updateQuestion(questionIdx, {
updateElement(elementIdx, {
type:
question.type === TSurveyElementTypeEnum.MultipleChoiceMulti
element.type === TSurveyElementTypeEnum.MultipleChoiceMulti
? TSurveyElementTypeEnum.MultipleChoiceSingle
: TSurveyElementTypeEnum.MultipleChoiceMulti,
});
}}>
{question.type === TSurveyElementTypeEnum.MultipleChoiceSingle
{element.type === TSurveyElementTypeEnum.MultipleChoiceSingle
? t("environments.surveys.edit.convert_to_multiple_choice")
: t("environments.surveys.edit.convert_to_single_choice")}
</Button>
<div className="flex flex-1 items-center justify-end gap-2">
<ShuffleOptionSelect
questionIdx={questionIdx}
shuffleOption={question.shuffleOption}
updateQuestion={updateQuestion}
elementIdx={elementIdx}
shuffleOption={element.shuffleOption}
updateElement={updateElement}
shuffleOptionsTypes={shuffleOptionsTypes}
/>
</div>

View File

@@ -8,15 +8,15 @@ import { TSurveyNPSElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
interface NPSQuestionFormProps {
interface NPSElementFormProps {
localSurvey: TSurvey;
question: TSurveyNPSElement;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyNPSElement>) => void;
element: TSurveyNPSElement;
elementIdx: number;
updateElement: (elementIdx: number, updatedAttributes: Partial<TSurveyNPSElement>) => void;
selectedLanguageCode: string;
setSelectedLanguageCode: (languageCode: string) => void;
isInvalid: boolean;
@@ -25,10 +25,10 @@ interface NPSQuestionFormProps {
isExternalUrlsAllowed?: boolean;
}
export const NPSQuestionForm = ({
question,
questionIdx,
updateQuestion,
export const NPSElementForm = ({
element,
elementIdx,
updateElement,
isInvalid,
localSurvey,
selectedLanguageCode,
@@ -36,59 +36,59 @@ export const NPSQuestionForm = ({
locale,
isStorageConfigured = true,
isExternalUrlsAllowed,
}: NPSQuestionFormProps): JSX.Element => {
}: NPSElementFormProps): JSX.Element => {
const { t } = useTranslation();
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const [parent] = useAutoAnimate();
return (
<form>
<QuestionFormInput
<ElementFormInput
id="headline"
value={question.headline}
value={element.headline}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.headline?.default || question.headline.default.trim() === ""}
autoFocus={!element.headline?.default || element.headline.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
<div ref={parent}>
{question.subheader !== undefined && (
{element.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
<ElementFormInput
id="subheader"
value={question.subheader}
value={element.subheader}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.subheader?.default || question.subheader.default.trim() === ""}
autoFocus={!element.subheader?.default || element.subheader.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
</div>
</div>
)}
{question.subheader === undefined && (
{element.subheader === undefined && (
<Button
size="sm"
variant="secondary"
className="mt-3"
type="button"
onClick={() => {
updateQuestion(questionIdx, {
updateElement(elementIdx, {
subheader: createI18nString("", surveyLanguageCodes),
});
}}>
@@ -100,14 +100,14 @@ export const NPSQuestionForm = ({
<div className="flex justify-between space-x-2">
<div className="w-full">
<QuestionFormInput
<ElementFormInput
id="lowerLabel"
value={question.lowerLabel}
value={element.lowerLabel}
label={t("environments.surveys.edit.lower_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
@@ -115,14 +115,14 @@ export const NPSQuestionForm = ({
/>
</div>
<div className="w-full">
<QuestionFormInput
<ElementFormInput
id="upperLabel"
value={question.upperLabel}
value={element.upperLabel}
label={t("environments.surveys.edit.upper_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
@@ -132,8 +132,8 @@ export const NPSQuestionForm = ({
</div>
<AdvancedOptionToggle
isChecked={question.isColorCodingEnabled}
onToggle={() => updateQuestion(questionIdx, { isColorCodingEnabled: !question.isColorCodingEnabled })}
isChecked={element.isColorCodingEnabled}
onToggle={() => updateElement(elementIdx, { isColorCodingEnabled: !element.isColorCodingEnabled })}
htmlId="isColorCodingEnabled"
title={t("environments.surveys.edit.add_color_coding")}
description={t("environments.surveys.edit.add_color_coding_description")}

View File

@@ -8,19 +8,19 @@ import { TSurveyOpenTextElement, TSurveyOpenTextElementInputType } from "@formbr
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
import { OptionsSwitch } from "@/modules/ui/components/options-switch";
interface OpenQuestionFormProps {
interface OpenElementFormProps {
localSurvey: TSurvey;
question: TSurveyOpenTextElement;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyOpenTextElement>) => void;
lastQuestion: boolean;
element: TSurveyOpenTextElement;
elementIdx: number;
updateElement: (elementIdx: number, updatedAttributes: Partial<TSurveyOpenTextElement>) => void;
lastElement: boolean;
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
@@ -29,10 +29,10 @@ interface OpenQuestionFormProps {
isExternalUrlsAllowed?: boolean;
}
export const OpenQuestionForm = ({
question,
questionIdx,
updateQuestion,
export const OpenElementForm = ({
element,
elementIdx,
updateElement,
isInvalid,
localSurvey,
selectedLanguageCode,
@@ -40,25 +40,25 @@ export const OpenQuestionForm = ({
locale,
isStorageConfigured = true,
isExternalUrlsAllowed,
}: OpenQuestionFormProps): JSX.Element => {
}: OpenElementFormProps): JSX.Element => {
const { t } = useTranslation();
const questionTypes = [
const elementTypes = [
{ value: "text", label: t("common.text"), icon: <MessageSquareTextIcon className="h-4 w-4" /> },
{ value: "email", label: t("common.email"), icon: <MailIcon className="h-4 w-4" /> },
{ value: "url", label: t("common.url"), icon: <LinkIcon className="h-4 w-4" /> },
{ value: "number", label: t("common.number"), icon: <HashIcon className="h-4 w-4" /> },
{ value: "phone", label: t("common.phone"), icon: <PhoneIcon className="h-4 w-4" /> },
];
const defaultPlaceholder = getPlaceholderByInputType(question.inputType ?? "text");
const defaultPlaceholder = getPlaceholderByInputType(element.inputType ?? "text");
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
const [showCharLimits, setShowCharLimits] = useState(question.inputType === "text");
const [showCharLimits, setShowCharLimits] = useState(element.inputType === "text");
const handleInputChange = (inputType: TSurveyOpenTextElementInputType) => {
const updatedAttributes = {
inputType: inputType,
placeholder: createI18nString(getPlaceholderByInputType(inputType), surveyLanguageCodes),
longAnswer: inputType === "text" ? question.longAnswer : false,
longAnswer: inputType === "text" ? element.longAnswer : false,
charLimit: {
min: undefined,
max: undefined,
@@ -66,68 +66,68 @@ export const OpenQuestionForm = ({
};
setIsCharLimitEnabled(false);
setShowCharLimits(inputType === "text");
updateQuestion(questionIdx, updatedAttributes);
updateElement(elementIdx, updatedAttributes);
};
const [parent] = useAutoAnimate();
const [isCharLimitEnabled, setIsCharLimitEnabled] = useState(false);
useEffect(() => {
if (question?.charLimit?.min !== undefined || question?.charLimit?.max !== undefined) {
if (element?.charLimit?.min !== undefined || element?.charLimit?.max !== undefined) {
setIsCharLimitEnabled(true);
} else {
setIsCharLimitEnabled(false);
}
}, [question?.charLimit?.max, question?.charLimit?.min]);
}, [element?.charLimit?.max, element?.charLimit?.min]);
return (
<form>
<QuestionFormInput
<ElementFormInput
id="headline"
value={question.headline}
value={element.headline}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.headline?.default || question.headline.default.trim() === ""}
autoFocus={!element.headline?.default || element.headline.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
<div ref={parent}>
{question.subheader !== undefined && (
{element.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
<ElementFormInput
id="subheader"
value={question.subheader}
value={element.subheader}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.subheader?.default || question.subheader.default.trim() === ""}
autoFocus={!element.subheader?.default || element.subheader.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
</div>
</div>
)}
{question.subheader === undefined && (
{element.subheader === undefined && (
<Button
size="sm"
variant="secondary"
className="mt-3"
type="button"
onClick={() => {
updateQuestion(questionIdx, {
updateElement(elementIdx, {
subheader: createI18nString("", surveyLanguageCodes),
});
}}>
@@ -137,17 +137,17 @@ export const OpenQuestionForm = ({
)}
</div>
<div className="mt-2">
<QuestionFormInput
<ElementFormInput
id="placeholder"
value={
question.placeholder
? question.placeholder
element.placeholder
? element.placeholder
: createI18nString(defaultPlaceholder, surveyLanguageCodes)
}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
label={t("common.placeholder")}
@@ -156,13 +156,13 @@ export const OpenQuestionForm = ({
/>
</div>
{/* Add a dropdown to select the question type */}
{/* Add a dropdown to select the element type */}
<div className="mt-3">
<Label htmlFor="questionType">{t("common.input_type")}</Label>
<Label htmlFor="elementType">{t("common.input_type")}</Label>
<div className="mt-2 flex items-center">
<OptionsSwitch
options={questionTypes}
currentOption={question.inputType}
options={elementTypes}
currentOption={element.inputType}
handleOptionChange={handleInputChange} // Use the merged function
/>
</div>
@@ -173,7 +173,7 @@ export const OpenQuestionForm = ({
isChecked={isCharLimitEnabled}
onToggle={(checked: boolean) => {
setIsCharLimitEnabled(checked);
updateQuestion(questionIdx, {
updateElement(elementIdx, {
charLimit: {
enabled: checked,
min: undefined,
@@ -181,7 +181,7 @@ export const OpenQuestionForm = ({
},
});
}}
htmlId={`charLimit-${question.id}`}
htmlId={`charLimit-${element.id}`}
description={t("environments.surveys.edit.character_limit_toggle_description")}
childBorder
title={t("environments.surveys.edit.character_limit_toggle_title")}
@@ -194,13 +194,13 @@ export const OpenQuestionForm = ({
name="minLength"
type="number"
min={0}
value={question?.charLimit?.min || ""}
value={element?.charLimit?.min || ""}
aria-label={t("common.minimum")}
className="bg-white"
onChange={(e) =>
updateQuestion(questionIdx, {
updateElement(elementIdx, {
charLimit: {
...question?.charLimit,
...element?.charLimit,
min: e.target.value ? parseInt(e.target.value) : undefined,
},
})
@@ -215,12 +215,12 @@ export const OpenQuestionForm = ({
type="number"
min={0}
aria-label={t("common.maximum")}
value={question?.charLimit?.max || ""}
value={element?.charLimit?.max || ""}
className="bg-white"
onChange={(e) =>
updateQuestion(questionIdx, {
updateElement(elementIdx, {
charLimit: {
...question?.charLimit,
...element?.charLimit,
max: e.target.value ? parseInt(e.target.value) : undefined,
},
})
@@ -232,16 +232,16 @@ export const OpenQuestionForm = ({
)}
<div className="mt-4">
<AdvancedOptionToggle
isChecked={question.longAnswer !== false}
isChecked={element.longAnswer !== false}
onToggle={(checked: boolean) => {
updateQuestion(questionIdx, {
updateElement(elementIdx, {
longAnswer: checked,
});
}}
htmlId={`longAnswer-${question.id}`}
htmlId={`longAnswer-${element.id}`}
title={t("environments.surveys.edit.long_answer")}
description={t("environments.surveys.edit.long_answer_toggle_description")}
disabled={question.inputType !== "text"}
disabled={element.inputType !== "text"}
customContainerClass="p-0"
/>
</div>

View File

@@ -6,21 +6,21 @@ import { IdBadge } from "@/modules/ui/components/id-badge";
import { Label } from "@/modules/ui/components/label";
interface OptionIdsProps {
question: TSurveyElement;
element: TSurveyElement;
selectedLanguageCode: string;
}
export const OptionIds = ({ question, selectedLanguageCode }: OptionIdsProps) => {
export const OptionIds = ({ element, selectedLanguageCode }: OptionIdsProps) => {
const { t } = useTranslation();
const renderChoiceIds = () => {
switch (question.type) {
switch (element.type) {
case TSurveyElementTypeEnum.MultipleChoiceSingle:
case TSurveyElementTypeEnum.MultipleChoiceMulti:
case TSurveyElementTypeEnum.Ranking:
return (
<div className="flex flex-col gap-2">
{question.choices.map((choice) => (
{element.choices.map((choice) => (
<div key={choice.id}>
<IdBadge id={choice.id} label={getLocalizedValue(choice.label, selectedLanguageCode)} />
</div>
@@ -31,7 +31,7 @@ export const OptionIds = ({ question, selectedLanguageCode }: OptionIdsProps) =>
case TSurveyElementTypeEnum.PictureSelection:
return (
<div className="flex flex-col gap-3">
{question.choices.map((choice) => {
{element.choices.map((choice) => {
const imageUrl = choice.imageUrl;
if (!imageUrl) return null;
return (

View File

@@ -10,7 +10,7 @@ import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { cn } from "@/lib/cn";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { Button } from "@/modules/ui/components/button";
import { FileInput } from "@/modules/ui/components/file-input";
import { Label } from "@/modules/ui/components/label";
@@ -18,9 +18,9 @@ import { Switch } from "@/modules/ui/components/switch";
interface PictureSelectionFormProps {
localSurvey: TSurvey;
question: TSurveyPictureSelectionElement;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyPictureSelectionElement>) => void;
element: TSurveyPictureSelectionElement;
elementIdx: number;
updateElement: (elementIdx: number, updatedAttributes: Partial<TSurveyPictureSelectionElement>) => void;
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
@@ -30,9 +30,9 @@ interface PictureSelectionFormProps {
export const PictureSelectionForm = ({
localSurvey,
question,
questionIdx,
updateQuestion,
element,
elementIdx,
updateElement,
selectedLanguageCode,
setSelectedLanguageCode,
isInvalid,
@@ -44,18 +44,18 @@ export const PictureSelectionForm = ({
const { t } = useTranslation();
const handleChoiceDeletion = (choiceValue: string) => {
// Filter out the deleted choice from the choices array
const newChoices = question.choices?.filter((choice) => choice.id !== choiceValue) || [];
const newChoices = element.choices?.filter((choice) => choice.id !== choiceValue) || [];
// Update the question with new choices and logic
updateQuestion(questionIdx, {
// Update the element with new choices and logic
updateElement(elementIdx, {
choices: newChoices,
});
};
const handleFileInputChanges = (urls: string[]) => {
// Handle choice deletion
if (urls.length < question.choices.length) {
const deletedChoice = question.choices.find((choice) => !urls.includes(choice.imageUrl));
if (urls.length < element.choices.length) {
const deletedChoice = element.choices.find((choice) => !urls.includes(choice.imageUrl));
if (deletedChoice) {
handleChoiceDeletion(deletedChoice.id);
}
@@ -63,11 +63,11 @@ export const PictureSelectionForm = ({
// Handle choice addition
const updatedChoices = urls.map((url) => {
const existingChoice = question.choices.find((choice) => choice.imageUrl === url);
const existingChoice = element.choices.find((choice) => choice.imageUrl === url);
return existingChoice ? { ...existingChoice } : { imageUrl: url, id: createId() };
});
updateQuestion(questionIdx, {
updateElement(elementIdx, {
choices: updatedChoices,
});
};
@@ -75,49 +75,49 @@ export const PictureSelectionForm = ({
const [parent] = useAutoAnimate();
return (
<form>
<QuestionFormInput
<ElementFormInput
id="headline"
value={question.headline}
value={element.headline}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.headline?.default || question.headline.default.trim() === ""}
autoFocus={!element.headline?.default || element.headline.default.trim() === ""}
/>
<div ref={parent}>
{question.subheader !== undefined && (
{element.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
<ElementFormInput
id="subheader"
value={question.subheader}
value={element.subheader}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.subheader?.default || question.subheader.default.trim() === ""}
autoFocus={!element.subheader?.default || element.subheader.default.trim() === ""}
/>
</div>
</div>
)}
{question.subheader === undefined && (
{element.subheader === undefined && (
<Button
size="sm"
variant="secondary"
className="mt-3"
type="button"
onClick={() => {
updateQuestion(questionIdx, {
updateElement(elementIdx, {
subheader: createI18nString("", surveyLanguageCodes),
});
}}>
@@ -131,7 +131,7 @@ export const PictureSelectionForm = ({
{t("common.images")}{" "}
<span
className={cn("text-slate-400", {
"text-red-600": isInvalid && question.choices?.length < 2,
"text-red-600": isInvalid && element.choices?.length < 2,
})}>
({t("environments.surveys.edit.upload_at_least_2_images")})
</span>
@@ -142,7 +142,7 @@ export const PictureSelectionForm = ({
allowedFileExtensions={["png", "jpeg", "jpg", "webp", "heic"]}
environmentId={environmentId}
onFileUpload={handleFileInputChanges}
fileUrl={question?.choices?.map((choice) => choice.imageUrl)}
fileUrl={element?.choices?.map((choice) => choice.imageUrl)}
multiple={true}
maxSizeInMB={5}
isStorageConfigured={isStorageConfigured}
@@ -153,10 +153,10 @@ export const PictureSelectionForm = ({
<div className="my-4 flex items-center space-x-2">
<Switch
id="multi-select-toggle"
checked={question.allowMulti}
checked={element.allowMulti}
onClick={(e) => {
e.stopPropagation();
updateQuestion(questionIdx, { allowMulti: !question.allowMulti });
updateElement(elementIdx, { allowMulti: !element.allowMulti });
}}
/>
<Label htmlFor="multi-select-toggle" className="cursor-pointer">

View File

@@ -12,17 +12,17 @@ import { TSurveyRankingElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { QuestionOptionChoice } from "@/modules/survey/editor/components/question-option-choice";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { ElementOptionChoice } from "@/modules/survey/editor/components/element-option-choice";
import { Button } from "@/modules/ui/components/button";
import { Label } from "@/modules/ui/components/label";
import { ShuffleOptionSelect } from "@/modules/ui/components/shuffle-option-select";
interface RankingQuestionFormProps {
interface RankingElementFormProps {
localSurvey: TSurvey;
question: TSurveyRankingElement;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyRankingElement>) => void;
element: TSurveyRankingElement;
elementIdx: number;
updateElement: (elementIdx: number, updatedAttributes: Partial<TSurveyRankingElement>) => void;
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
@@ -31,10 +31,10 @@ interface RankingQuestionFormProps {
isExternalUrlsAllowed?: boolean;
}
export const RankingQuestionForm = ({
question,
questionIdx,
updateQuestion,
export const RankingElementForm = ({
element,
elementIdx,
updateElement,
isInvalid,
localSurvey,
selectedLanguageCode,
@@ -42,7 +42,7 @@ export const RankingQuestionForm = ({
locale,
isStorageConfigured = true,
isExternalUrlsAllowed,
}: RankingQuestionFormProps): JSX.Element => {
}: RankingElementFormProps): JSX.Element => {
const { t } = useTranslation();
const lastChoiceRef = useRef<HTMLInputElement>(null);
const [isInvalidValue, setIsInvalidValue] = useState<string | null>(null);
@@ -51,31 +51,31 @@ export const RankingQuestionForm = ({
const surveyLanguages = localSurvey.languages ?? [];
const updateChoice = (choiceIdx: number, updatedAttributes: { label: TI18nString }) => {
if (question.choices) {
const newChoices = question.choices.map((choice, idx) => {
if (element.choices) {
const newChoices = element.choices.map((choice, idx) => {
if (idx !== choiceIdx) return choice;
return { ...choice, ...updatedAttributes };
});
updateQuestion(questionIdx, { choices: newChoices });
updateElement(elementIdx, { choices: newChoices });
}
};
const addChoice = (choiceIdx: number) => {
let newChoices = !question.choices ? [] : question.choices;
let newChoices = !element.choices ? [] : element.choices;
const newChoice = {
id: createId(),
label: createI18nString("", surveyLanguageCodes),
};
updateQuestion(questionIdx, {
updateElement(elementIdx, {
choices: [...newChoices.slice(0, choiceIdx + 1), newChoice, ...newChoices.slice(choiceIdx + 1)],
});
};
const addOption = () => {
const choices = !question.choices ? [] : question.choices;
const choices = !element.choices ? [] : element.choices;
if (choices.length >= 25) {
return;
@@ -86,18 +86,18 @@ export const RankingQuestionForm = ({
label: createI18nString("", surveyLanguageCodes),
};
updateQuestion(questionIdx, { choices: [...choices, newChoice] });
updateElement(elementIdx, { choices: [...choices, newChoice] });
};
const deleteChoice = (choiceIdx: number) => {
const newChoices = !question.choices ? [] : question.choices.filter((_, idx) => idx !== choiceIdx);
const choiceValue = question.choices[choiceIdx].label[selectedLanguageCode];
const newChoices = !element.choices ? [] : element.choices.filter((_, idx) => idx !== choiceIdx);
const choiceValue = element.choices[choiceIdx].label[selectedLanguageCode];
if (isInvalidValue === choiceValue) {
setIsInvalidValue(null);
}
updateQuestion(questionIdx, { choices: newChoices });
updateElement(elementIdx, { choices: newChoices });
};
const shuffleOptionsTypes = {
@@ -109,7 +109,7 @@ export const RankingQuestionForm = ({
all: {
id: "all",
label: t("environments.surveys.edit.randomize_all"),
show: question.choices.length > 0,
show: element.choices.length > 0,
},
};
@@ -117,58 +117,58 @@ export const RankingQuestionForm = ({
if (lastChoiceRef.current) {
lastChoiceRef.current?.focus();
}
}, [question.choices?.length]);
}, [element.choices?.length]);
const [parent] = useAutoAnimate();
return (
<form>
<QuestionFormInput
<ElementFormInput
id="headline"
value={question.headline}
value={element.headline}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.headline?.default || question.headline.default.trim() === ""}
autoFocus={!element.headline?.default || element.headline.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
<div ref={parent}>
{question.subheader !== undefined && (
{element.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
<ElementFormInput
id="subheader"
value={question.subheader}
value={element.subheader}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.subheader?.default || question.subheader.default.trim() === ""}
autoFocus={!element.subheader?.default || element.subheader.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
</div>
</div>
)}
{question.subheader === undefined && (
{element.subheader === undefined && (
<Button
size="sm"
variant="secondary"
className="mt-3"
type="button"
onClick={() => {
updateQuestion(questionIdx, {
updateElement(elementIdx, {
subheader: createI18nString("", surveyLanguageCodes),
});
}}>
@@ -190,24 +190,24 @@ export const RankingQuestionForm = ({
return;
}
const activeIndex = question.choices.findIndex((choice) => choice.id === active.id);
const overIndex = question.choices.findIndex((choice) => choice.id === over.id);
const activeIndex = element.choices.findIndex((choice) => choice.id === active.id);
const overIndex = element.choices.findIndex((choice) => choice.id === over.id);
const newChoices = [...question.choices];
const newChoices = [...element.choices];
newChoices.splice(activeIndex, 1);
newChoices.splice(overIndex, 0, question.choices[activeIndex]);
newChoices.splice(overIndex, 0, element.choices[activeIndex]);
updateQuestion(questionIdx, { choices: newChoices });
updateElement(elementIdx, { choices: newChoices });
}}>
<SortableContext items={question.choices} strategy={verticalListSortingStrategy}>
<SortableContext items={element.choices} strategy={verticalListSortingStrategy}>
<div className="flex flex-col gap-2" ref={parent}>
{question.choices?.map((choice, choiceIdx) => (
<QuestionOptionChoice
{element.choices?.map((choice, choiceIdx) => (
<ElementOptionChoice
key={choice.id}
choice={choice}
choiceIdx={choiceIdx}
questionIdx={questionIdx}
elementIdx={elementIdx}
updateChoice={updateChoice}
deleteChoice={deleteChoice}
addChoice={addChoice}
@@ -216,8 +216,8 @@ export const RankingQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
surveyLanguages={surveyLanguages}
question={question}
updateQuestion={updateQuestion}
element={element}
updateElement={updateElement}
surveyLanguageCodes={surveyLanguageCodes}
locale={locale}
isStorageConfigured={isStorageConfigured}
@@ -232,16 +232,16 @@ export const RankingQuestionForm = ({
size="sm"
variant="secondary"
type="button"
disabled={question.choices?.length >= 25}
disabled={element.choices?.length >= 25}
onClick={() => addOption()}>
{t("environments.surveys.edit.add_option")}
<PlusIcon />
</Button>
<ShuffleOptionSelect
shuffleOptionsTypes={shuffleOptionsTypes}
updateQuestion={updateQuestion}
shuffleOption={question.shuffleOption}
questionIdx={questionIdx}
updateElement={updateElement}
shuffleOption={element.shuffleOption}
elementIdx={elementIdx}
/>
</div>
</div>

View File

@@ -7,18 +7,18 @@ import { TSurveyRatingElement } from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
import { QuestionFormInput } from "@/modules/survey/components/question-form-input";
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
import { Dropdown } from "@/modules/survey/editor/components/rating-type-dropdown";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
import { Label } from "@/modules/ui/components/label";
interface RatingQuestionFormProps {
interface RatingElementFormProps {
localSurvey: TSurvey;
question: TSurveyRatingElement;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyRatingElement>) => void;
lastQuestion: boolean;
element: TSurveyRatingElement;
elementIdx: number;
updateElement: (elementIdx: number, updatedAttributes: Partial<TSurveyRatingElement>) => void;
lastElement: boolean;
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
@@ -27,10 +27,10 @@ interface RatingQuestionFormProps {
isExternalUrlsAllowed?: boolean;
}
export const RatingQuestionForm = ({
question,
questionIdx,
updateQuestion,
export const RatingElementForm = ({
element,
elementIdx,
updateElement,
isInvalid,
localSurvey,
selectedLanguageCode,
@@ -38,59 +38,59 @@ export const RatingQuestionForm = ({
locale,
isStorageConfigured = true,
isExternalUrlsAllowed,
}: RatingQuestionFormProps) => {
}: RatingElementFormProps) => {
const { t } = useTranslation();
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const [parent] = useAutoAnimate();
return (
<form>
<QuestionFormInput
<ElementFormInput
id="headline"
value={question.headline}
value={element.headline}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.headline?.default || question.headline.default.trim() === ""}
autoFocus={!element.headline?.default || element.headline.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
<div ref={parent}>
{question.subheader !== undefined && (
{element.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
<ElementFormInput
id="subheader"
value={question.subheader}
value={element.subheader}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
isStorageConfigured={isStorageConfigured}
autoFocus={!question.subheader?.default || question.subheader.default.trim() === ""}
autoFocus={!element.subheader?.default || element.subheader.default.trim() === ""}
isExternalUrlsAllowed={isExternalUrlsAllowed}
/>
</div>
</div>
)}
{question.subheader === undefined && (
{element.subheader === undefined && (
<Button
size="sm"
variant="secondary"
className="mt-3"
type="button"
onClick={() => {
updateQuestion(questionIdx, {
updateElement(elementIdx, {
subheader: createI18nString("", surveyLanguageCodes),
});
}}>
@@ -110,13 +110,13 @@ export const RatingQuestionForm = ({
{ label: t("environments.surveys.edit.star"), value: "star", icon: StarIcon },
{ label: t("environments.surveys.edit.smiley"), value: "smiley", icon: SmileIcon },
]}
defaultValue={question.scale || "number"}
defaultValue={element.scale || "number"}
onSelect={(option) => {
if (option.value === "star") {
updateQuestion(questionIdx, { scale: option.value, isColorCodingEnabled: false });
updateElement(elementIdx, { scale: option.value, isColorCodingEnabled: false });
return;
}
updateQuestion(questionIdx, { scale: option.value as "number" | "smiley" | "star" });
updateElement(elementIdx, { scale: option.value as "number" | "smiley" | "star" });
}}
/>
</div>
@@ -134,9 +134,9 @@ export const RatingQuestionForm = ({
{ label: t("environments.surveys.edit.ten_points"), value: 10 },
]}
/* disabled={survey.status !== "draft"} */
defaultValue={question.range || 5}
defaultValue={element.range || 5}
onSelect={(option) =>
updateQuestion(questionIdx, { range: option.value as TSurveyRatingElement["range"] })
updateElement(elementIdx, { range: option.value as TSurveyRatingElement["range"] })
}
/>
</div>
@@ -145,15 +145,15 @@ export const RatingQuestionForm = ({
<div className="flex justify-between gap-8">
<div className="flex-1">
<QuestionFormInput
<ElementFormInput
id="lowerLabel"
placeholder="Not good"
value={question.lowerLabel}
value={element.lowerLabel}
label={t("environments.surveys.edit.lower_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
@@ -161,15 +161,15 @@ export const RatingQuestionForm = ({
/>
</div>
<div className="flex-1">
<QuestionFormInput
<ElementFormInput
id="upperLabel"
placeholder="Very satisfied"
value={question.upperLabel}
value={element.upperLabel}
label={t("environments.surveys.edit.upper_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
elementIdx={elementIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
updateElement={updateElement}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
@@ -178,12 +178,10 @@ export const RatingQuestionForm = ({
</div>
</div>
{question.scale !== "star" && (
{element.scale !== "star" && (
<AdvancedOptionToggle
isChecked={question.isColorCodingEnabled}
onToggle={() =>
updateQuestion(questionIdx, { isColorCodingEnabled: !question.isColorCodingEnabled })
}
isChecked={element.isColorCodingEnabled}
onToggle={() => updateElement(elementIdx, { isColorCodingEnabled: !element.isColorCodingEnabled })}
htmlId="isColorCodingEnabled"
title={t("environments.surveys.edit.add_color_coding")}
description={t("environments.surveys.edit.add_color_coding_description")}

View File

@@ -4,7 +4,7 @@ import { useRef } from "react";
import { useTranslation } from "react-i18next";
import { TSurvey, TSurveyRedirectUrlCard } from "@formbricks/types/surveys/types";
import { headlineToRecall, recallToHeadline } from "@/lib/utils/recall";
import { RecallWrapper } from "@/modules/survey/components/question-form-input/components/recall-wrapper";
import { RecallWrapper } from "@/modules/survey/components/element-form-input/components/recall-wrapper";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
@@ -25,7 +25,7 @@ export const RedirectUrlForm = ({ localSurvey, endingCard, updateSurvey }: Redir
<Label>{t("common.url")}</Label>
<RecallWrapper
value={endingCard.url ?? ""}
questionId={endingCard.id}
elementId={endingCard.id}
onChange={(val, recallItems, fallbacks) => {
const updatedValue = {
...endingCard,

View File

@@ -33,7 +33,7 @@ export const SurveyEditorTabs = ({
const tabsComputed = useMemo(() => {
const tabs: Tab[] = [
{
id: "questions",
id: "elements",
label: t("common.questions"),
icon: <Rows3Icon className="h-5 w-5" />,
},
@@ -59,7 +59,7 @@ export const SurveyEditorTabs = ({
return tabs;
}
return tabs.filter((tab) => tab.id !== "styling");
}, [isStylingTabVisible, isSurveyFollowUpsAllowed]);
}, [isStylingTabVisible, isSurveyFollowUpsAllowed, t]);
// Hide settings tab in CX mode
let tabsToDisplay = isCxMode ? tabsComputed.filter((tab) => tab.id !== "settings") : tabsComputed;

View File

@@ -12,8 +12,8 @@ import { structuredClone } from "@/lib/pollyfills/structuredClone";
import { useDocumentVisibility } from "@/lib/useDocumentVisibility";
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
import { EditPublicSurveyAlertDialog } from "@/modules/survey/components/edit-public-survey-alert-dialog";
import { ElementsView } from "@/modules/survey/editor/components/elements-view";
import { LoadingSkeleton } from "@/modules/survey/editor/components/loading-skeleton";
import { QuestionsView } from "@/modules/survey/editor/components/questions-view";
import { SettingsView } from "@/modules/survey/editor/components/settings-view";
import { StylingView } from "@/modules/survey/editor/components/styling-view";
import { SurveyEditorTabs } from "@/modules/survey/editor/components/survey-editor-tabs";
@@ -80,10 +80,10 @@ export const SurveyEditor = ({
quotas,
isExternalUrlsAllowed,
}: SurveyEditorProps) => {
const [activeView, setActiveView] = useState<TSurveyEditorTabs>("questions");
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
const [activeView, setActiveView] = useState<TSurveyEditorTabs>("elements");
const [activeElementId, setActiveElementId] = useState<string | null>(null);
const [localSurvey, setLocalSurvey] = useState<TSurvey | null>(() => structuredClone(survey));
const [invalidQuestions, setInvalidQuestions] = useState<string[] | null>(null);
const [invalidElements, setInvalidElements] = useState<string[] | null>(null);
const [selectedLanguageCode, setSelectedLanguageCode] = useState<string>("default");
const surveyEditorRef = useRef(null);
const [localProject, setLocalProject] = useState<Project>(project);
@@ -112,7 +112,7 @@ export const SurveyEditor = ({
// Set first element from first block
const firstBlock = survey.blocks[0];
if (firstBlock) {
setActiveQuestionId(firstBlock.elements?.[0]?.id);
setActiveElementId(firstBlock.elements?.[0]?.id);
}
}
@@ -137,11 +137,11 @@ export const SurveyEditor = ({
};
}, [localProject.id]);
// when the survey type changes, we need to reset the active question id to the first question
// when the survey type changes, we need to reset the active element id to the first element
useEffect(() => {
const firstBlock = localSurvey?.blocks[0];
if (firstBlock) {
setActiveQuestionId(firstBlock.elements[0]?.id);
setActiveElementId(firstBlock.elements[0]?.id);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [localSurvey?.type]);
@@ -167,7 +167,7 @@ export const SurveyEditor = ({
environmentId={environment.id}
activeId={activeView}
setActiveId={setActiveView}
setInvalidQuestions={setInvalidQuestions}
setInvalidElements={setInvalidElements}
project={localProject}
responseCount={responseCount}
selectedLanguageCode={selectedLanguageCode}
@@ -189,16 +189,16 @@ export const SurveyEditor = ({
isSurveyFollowUpsAllowed={isSurveyFollowUpsAllowed}
/>
{activeView === "questions" && (
<QuestionsView
{activeView === "elements" && (
<ElementsView
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}
activeElementId={activeElementId}
setActiveElementId={setActiveElementId}
project={localProject}
projectLanguages={projectLanguages}
invalidQuestions={invalidQuestions}
setInvalidQuestions={setInvalidQuestions}
invalidElements={invalidElements}
setInvalidElements={setInvalidElements}
selectedLanguageCode={selectedLanguageCode || "default"}
setSelectedLanguageCode={setSelectedLanguageCode}
isMultiLanguageAllowed={isMultiLanguageAllowed}
@@ -266,7 +266,7 @@ export const SurveyEditor = ({
<aside className="group hidden flex-1 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-200 bg-slate-100 shadow-inner md:flex md:flex-col">
<PreviewSurvey
survey={localSurvey}
questionId={activeQuestionId}
elementId={activeElementId}
project={localProject}
environment={environment}
previewType={localSurvey.type === "app" ? "modal" : "fullwidth"}

View File

@@ -33,7 +33,7 @@ interface SurveyMenuBarProps {
environmentId: string;
activeId: TSurveyEditorTabs;
setActiveId: React.Dispatch<React.SetStateAction<TSurveyEditorTabs>>;
setInvalidQuestions: React.Dispatch<React.SetStateAction<string[]>>;
setInvalidElements: React.Dispatch<React.SetStateAction<string[]>>;
project: Project;
responseCount: number;
selectedLanguageCode: string;
@@ -51,7 +51,7 @@ export const SurveyMenuBar = ({
setLocalSurvey,
activeId,
setActiveId,
setInvalidQuestions,
setInvalidElements,
project,
responseCount,
selectedLanguageCode,
@@ -183,22 +183,20 @@ export const SurveyMenuBar = ({
const element = block?.elements[elementIdx];
if (element) {
setInvalidQuestions((prevInvalidQuestions) =>
prevInvalidQuestions ? [...prevInvalidQuestions, element.id] : [element.id]
setInvalidElements((prevInvalidElements) =>
prevInvalidElements ? [...prevInvalidElements, element.id] : [element.id]
);
}
}
// For block-level errors (buttonLabel, logic, etc.), we don't mark specific questions as invalid
// The error will still be shown in the toast/UI via the error message
} else if (currentError.path[0] === "welcomeCard") {
setInvalidQuestions((prevInvalidQuestions) =>
prevInvalidQuestions ? [...prevInvalidQuestions, "start"] : ["start"]
setInvalidElements((prevInvalidElements) =>
prevInvalidElements ? [...prevInvalidElements, "start"] : ["start"]
);
} else if (currentError.path[0] === "endings") {
const endingIdx = typeof currentError.path[1] === "number" ? currentError.path[1] : -1;
setInvalidQuestions((prevInvalidQuestions) =>
prevInvalidQuestions
? [...prevInvalidQuestions, localSurvey.endings[endingIdx].id]
setInvalidElements((prevInvalidElements) =>
prevInvalidElements
? [...prevInvalidElements, localSurvey.endings[endingIdx].id]
: [localSurvey.endings[endingIdx].id]
);
}

View File

@@ -79,30 +79,30 @@ export const SurveyVariablesCardItem = ({
// Removed auto-submit effect
const onVariableDelete = (variableToDelete: TSurveyVariable) => {
const questions = getElementsFromBlocks(localSurvey.blocks);
const quesIdx = findVariableUsedInLogic(localSurvey, variableToDelete.id);
const elements = getElementsFromBlocks(localSurvey.blocks);
const elementIdx = findVariableUsedInLogic(localSurvey, variableToDelete.id);
if (quesIdx !== -1) {
if (elementIdx !== -1) {
toast.error(
t(
"environments.surveys.edit.variable_is_used_in_logic_of_question_please_remove_it_from_logic_first",
{
variable: variableToDelete.name,
questionIndex: quesIdx + 1,
questionIndex: elementIdx + 1,
}
)
);
return;
}
const recallQuestionIdx = isUsedInRecall(localSurvey, variableToDelete.id);
if (recallQuestionIdx === -2) {
const recallElementIdx = isUsedInRecall(localSurvey, variableToDelete.id);
if (recallElementIdx === -2) {
toast.error(
t("environments.surveys.edit.variable_used_in_recall_welcome", { variable: variableToDelete.name })
);
return;
}
if (recallQuestionIdx === questions.length) {
if (recallElementIdx === elements.length) {
toast.error(
t("environments.surveys.edit.variable_used_in_recall_ending_card", {
variable: variableToDelete.name,
@@ -111,11 +111,11 @@ export const SurveyVariablesCardItem = ({
return;
}
if (recallQuestionIdx !== -1) {
if (recallElementIdx !== -1) {
toast.error(
t("environments.surveys.edit.variable_used_in_recall", {
variable: variableToDelete.name,
questionIndex: recallQuestionIdx + 1,
questionIndex: recallElementIdx + 1,
})
);
return;

View File

@@ -5,15 +5,15 @@ import * as Collapsible from "@radix-ui/react-collapsible";
import { FileDigitIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { TSurveyQuota } from "@formbricks/types/quota";
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { TSurvey } from "@formbricks/types/surveys/types";
import { cn } from "@/lib/cn";
import { SurveyVariablesCardItem } from "@/modules/survey/editor/components/survey-variables-card-item";
interface SurveyVariablesCardProps {
localSurvey: TSurvey;
setLocalSurvey: (survey: TSurvey) => void;
activeQuestionId: TSurveyQuestionId | null;
setActiveQuestionId: (id: TSurveyQuestionId | null) => void;
activeElementId: string | null;
setActiveElementId: (id: string | null) => void;
quotas: TSurveyQuota[];
}
@@ -22,20 +22,20 @@ const variablesCardId = `fb-variables-${Date.now()}`;
export const SurveyVariablesCard = ({
localSurvey,
setLocalSurvey,
activeQuestionId,
setActiveQuestionId,
activeElementId,
setActiveElementId,
quotas,
}: SurveyVariablesCardProps) => {
const open = activeQuestionId === variablesCardId;
const open = activeElementId === variablesCardId;
const { t } = useTranslation();
const [parent] = useAutoAnimate();
const setOpenState = (state: boolean) => {
if (state) {
// NOSONAR // This is ok for setOpenState
setActiveQuestionId(variablesCardId);
setActiveElementId(variablesCardId);
} else {
setActiveQuestionId(null);
setActiveElementId(null);
}
};

View File

@@ -11,22 +11,17 @@ import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
interface UpdateQuestionIdProps {
interface UpdatElementIdProps {
localSurvey: TSurvey;
question: TSurveyElement;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
element: TSurveyElement;
elementIdx: number;
updateElement: (elementIdx: number, updatedAttributes: any) => void;
}
export const UpdateQuestionId = ({
localSurvey,
question,
questionIdx,
updateQuestion,
}: UpdateQuestionIdProps) => {
export const UpdateElementId = ({ localSurvey, element, elementIdx, updateElement }: UpdatElementIdProps) => {
const { t } = useTranslation();
const [currentValue, setCurrentValue] = useState(question.id);
const [prevValue, setPrevValue] = useState(question.id);
const [currentValue, setCurrentValue] = useState(element.id);
const [prevValue, setPrevValue] = useState(element.id);
const [isInputInvalid, setIsInputInvalid] = useState(
currentValue.trim() === "" || currentValue.includes(" ")
);
@@ -37,12 +32,12 @@ export const UpdateQuestionId = ({
return;
}
const questions = getElementsFromBlocks(localSurvey.blocks);
const questionIds = questions.map((q) => q.id);
const elements = getElementsFromBlocks(localSurvey.blocks);
const elementIds = elements.map((q) => q.id);
const endingCardIds = localSurvey.endings.map((e) => e.id);
const hiddenFieldIds = localSurvey.hiddenFields.fieldIds ?? [];
const validateIdError = validateId("Question", currentValue, questionIds, endingCardIds, hiddenFieldIds);
const validateIdError = validateId("Element", currentValue, elementIds, endingCardIds, hiddenFieldIds);
if (validateIdError) {
setIsInputInvalid(true);
@@ -53,28 +48,28 @@ export const UpdateQuestionId = ({
setIsInputInvalid(false);
toast.success(t("environments.surveys.edit.question_id_updated"));
updateQuestion(questionIdx, { id: currentValue });
updateElement(elementIdx, { id: currentValue });
setPrevValue(currentValue); // after successful update, set current value as previous value
};
const isButtonDisabled = () => {
if (currentValue === question.id || currentValue.trim() === "") return true;
if (currentValue === element.id || currentValue.trim() === "") return true;
else return false;
};
return (
<div>
<Label htmlFor="questionId">{t("common.question_id")}</Label>
<Label htmlFor="elementId">{t("common.question_id")}</Label>
<div className="mt-2 inline-flex w-full items-center space-x-2">
<Input
id="questionId"
name="questionId"
id="elementId"
name="elementId"
value={currentValue}
onChange={(e) => {
setCurrentValue(e.target.value);
}}
dir="auto"
disabled={localSurvey.status !== "draft" && !question.isDraft}
disabled={localSurvey.status !== "draft" && !element.isDraft}
className={`h-10 ${isInputInvalid ? "border-red-300 focus:border-red-300" : ""}`}
/>
<Button size="sm" onClick={saveAction} disabled={isButtonDisabled()}>

View File

@@ -11,7 +11,7 @@ const logicRules = getLogicRules(mockT as unknown as TFunction);
describe("getLogicRules", () => {
test("should return correct structure for question rules", () => {
expect(logicRules).toHaveProperty("question");
expect(logicRules.question).toBeInstanceOf(Object);
expect(logicRules.element).toBeInstanceOf(Object);
});
test("should return correct structure for variable rules", () => {
@@ -35,7 +35,7 @@ describe("getLogicRules", () => {
describe("Question Specific Rules", () => {
test("OpenText.text", () => {
const openTextTextRules = logicRules.question[TSurveyQuestionTypeEnum.OpenText + ".text"];
const openTextTextRules = logicRules.element[TSurveyQuestionTypeEnum.OpenText + ".text"];
expect(openTextTextRules).toBeDefined();
expect(openTextTextRules.options).toEqual([
{
@@ -82,7 +82,7 @@ describe("getLogicRules", () => {
});
test("OpenText.number", () => {
const openTextNumberRules = logicRules.question[TSurveyQuestionTypeEnum.OpenText + ".number"];
const openTextNumberRules = logicRules.element[TSurveyQuestionTypeEnum.OpenText + ".number"];
expect(openTextNumberRules).toBeDefined();
expect(openTextNumberRules.options).toEqual([
{ label: "=", value: ZSurveyLogicConditionsOperator.Enum.equals },
@@ -103,7 +103,7 @@ describe("getLogicRules", () => {
});
test("MultipleChoiceSingle", () => {
const rules = logicRules.question[TSurveyQuestionTypeEnum.MultipleChoiceSingle];
const rules = logicRules.element[TSurveyQuestionTypeEnum.MultipleChoiceSingle];
expect(rules).toBeDefined();
expect(rules.options).toEqual([
{
@@ -130,7 +130,7 @@ describe("getLogicRules", () => {
});
test("MultipleChoiceMulti", () => {
const rules = logicRules.question[TSurveyQuestionTypeEnum.MultipleChoiceMulti];
const rules = logicRules.element[TSurveyQuestionTypeEnum.MultipleChoiceMulti];
expect(rules).toBeDefined();
expect(rules.options).toEqual([
{
@@ -169,7 +169,7 @@ describe("getLogicRules", () => {
});
test("PictureSelection", () => {
const rules = logicRules.question[TSurveyQuestionTypeEnum.PictureSelection];
const rules = logicRules.element[TSurveyQuestionTypeEnum.PictureSelection];
expect(rules).toBeDefined();
expect(rules.options).toEqual([
{
@@ -208,7 +208,7 @@ describe("getLogicRules", () => {
});
test("Rating", () => {
const rules = logicRules.question[TSurveyQuestionTypeEnum.Rating];
const rules = logicRules.element[TSurveyQuestionTypeEnum.Rating];
expect(rules).toBeDefined();
expect(rules.options).toEqual([
{ label: "=", value: ZSurveyLogicConditionsOperator.Enum.equals },
@@ -229,7 +229,7 @@ describe("getLogicRules", () => {
});
test("NPS", () => {
const rules = logicRules.question[TSurveyQuestionTypeEnum.NPS];
const rules = logicRules.element[TSurveyQuestionTypeEnum.NPS];
expect(rules).toBeDefined();
expect(rules.options).toEqual([
{ label: "=", value: ZSurveyLogicConditionsOperator.Enum.equals },
@@ -250,7 +250,7 @@ describe("getLogicRules", () => {
});
test("CTA", () => {
const rules = logicRules.question[TSurveyQuestionTypeEnum.CTA];
const rules = logicRules.element[TSurveyQuestionTypeEnum.CTA];
expect(rules).toBeDefined();
expect(rules.options).toEqual([
{
@@ -265,7 +265,7 @@ describe("getLogicRules", () => {
});
test("Consent", () => {
const rules = logicRules.question[TSurveyQuestionTypeEnum.Consent];
const rules = logicRules.element[TSurveyQuestionTypeEnum.Consent];
expect(rules).toBeDefined();
expect(rules.options).toEqual([
{
@@ -280,7 +280,7 @@ describe("getLogicRules", () => {
});
test("Date", () => {
const rules = logicRules.question[TSurveyQuestionTypeEnum.Date];
const rules = logicRules.element[TSurveyQuestionTypeEnum.Date];
expect(rules).toBeDefined();
expect(rules.options).toEqual([
{
@@ -311,7 +311,7 @@ describe("getLogicRules", () => {
});
test("FileUpload", () => {
const rules = logicRules.question[TSurveyQuestionTypeEnum.FileUpload];
const rules = logicRules.element[TSurveyQuestionTypeEnum.FileUpload];
expect(rules).toBeDefined();
expect(rules.options).toEqual([
{
@@ -326,7 +326,7 @@ describe("getLogicRules", () => {
});
test("Ranking", () => {
const rules = logicRules.question[TSurveyQuestionTypeEnum.Ranking];
const rules = logicRules.element[TSurveyQuestionTypeEnum.Ranking];
expect(rules).toBeDefined();
expect(rules.options).toEqual([
{
@@ -341,7 +341,7 @@ describe("getLogicRules", () => {
});
test("Cal", () => {
const rules = logicRules.question[TSurveyQuestionTypeEnum.Cal];
const rules = logicRules.element[TSurveyQuestionTypeEnum.Cal];
expect(rules).toBeDefined();
expect(rules.options).toEqual([
{
@@ -356,7 +356,7 @@ describe("getLogicRules", () => {
});
test("Matrix", () => {
const rules = logicRules.question[TSurveyQuestionTypeEnum.Matrix];
const rules = logicRules.element[TSurveyQuestionTypeEnum.Matrix];
expect(rules).toBeDefined();
expect(rules.options).toEqual([
{
@@ -375,7 +375,7 @@ describe("getLogicRules", () => {
});
test("Matrix.row", () => {
const rules = logicRules.question[TSurveyQuestionTypeEnum.Matrix + ".row"];
const rules = logicRules.element[TSurveyQuestionTypeEnum.Matrix + ".row"];
expect(rules).toBeDefined();
expect(rules.options).toEqual([
{
@@ -402,7 +402,7 @@ describe("getLogicRules", () => {
});
test("Address", () => {
const rules = logicRules.question[TSurveyQuestionTypeEnum.Address];
const rules = logicRules.element[TSurveyQuestionTypeEnum.Address];
expect(rules).toBeDefined();
expect(rules.options).toEqual([
{
@@ -417,7 +417,7 @@ describe("getLogicRules", () => {
});
test("ContactInfo", () => {
const rules = logicRules.question[TSurveyQuestionTypeEnum.ContactInfo];
const rules = logicRules.element[TSurveyQuestionTypeEnum.ContactInfo];
expect(rules).toBeDefined();
expect(rules.options).toEqual([
{

View File

@@ -1,11 +1,11 @@
import { TFunction } from "i18next";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { ZSurveyLogicConditionsOperator } from "@formbricks/types/surveys/logic";
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
export const getLogicRules = (t: TFunction) => {
return {
question: {
[`${TSurveyQuestionTypeEnum.OpenText}.text`]: {
element: {
[`${TSurveyElementTypeEnum.OpenText}.text`]: {
options: [
{
label: t("environments.surveys.edit.equals"),
@@ -49,7 +49,7 @@ export const getLogicRules = (t: TFunction) => {
},
],
},
[`${TSurveyQuestionTypeEnum.OpenText}.number`]: {
[`${TSurveyElementTypeEnum.OpenText}.number`]: {
options: [
{
label: "=",
@@ -85,7 +85,7 @@ export const getLogicRules = (t: TFunction) => {
},
],
},
[TSurveyQuestionTypeEnum.MultipleChoiceSingle]: {
[TSurveyElementTypeEnum.MultipleChoiceSingle]: {
options: [
{
label: t("environments.surveys.edit.equals"),
@@ -109,7 +109,7 @@ export const getLogicRules = (t: TFunction) => {
},
],
},
[TSurveyQuestionTypeEnum.MultipleChoiceMulti]: {
[TSurveyElementTypeEnum.MultipleChoiceMulti]: {
options: [
{
label: t("environments.surveys.edit.equals"),
@@ -145,7 +145,7 @@ export const getLogicRules = (t: TFunction) => {
},
],
},
[TSurveyQuestionTypeEnum.PictureSelection]: {
[TSurveyElementTypeEnum.PictureSelection]: {
options: [
{
label: t("environments.surveys.edit.equals"),
@@ -181,7 +181,7 @@ export const getLogicRules = (t: TFunction) => {
},
],
},
[TSurveyQuestionTypeEnum.Rating]: {
[TSurveyElementTypeEnum.Rating]: {
options: [
{
label: "=",
@@ -217,7 +217,7 @@ export const getLogicRules = (t: TFunction) => {
},
],
},
[TSurveyQuestionTypeEnum.NPS]: {
[TSurveyElementTypeEnum.NPS]: {
options: [
{
label: "=",
@@ -253,7 +253,7 @@ export const getLogicRules = (t: TFunction) => {
},
],
},
[TSurveyQuestionTypeEnum.CTA]: {
[TSurveyElementTypeEnum.CTA]: {
options: [
{
label: t("environments.surveys.edit.is_clicked"),
@@ -265,7 +265,7 @@ export const getLogicRules = (t: TFunction) => {
},
],
},
[TSurveyQuestionTypeEnum.Consent]: {
[TSurveyElementTypeEnum.Consent]: {
options: [
{
label: t("environments.surveys.edit.is_accepted"),
@@ -277,7 +277,7 @@ export const getLogicRules = (t: TFunction) => {
},
],
},
[TSurveyQuestionTypeEnum.Date]: {
[TSurveyElementTypeEnum.Date]: {
options: [
{
label: t("environments.surveys.edit.equals"),
@@ -305,7 +305,7 @@ export const getLogicRules = (t: TFunction) => {
},
],
},
[TSurveyQuestionTypeEnum.FileUpload]: {
[TSurveyElementTypeEnum.FileUpload]: {
options: [
{
label: t("environments.surveys.edit.is_submitted"),
@@ -317,7 +317,7 @@ export const getLogicRules = (t: TFunction) => {
},
],
},
[TSurveyQuestionTypeEnum.Ranking]: {
[TSurveyElementTypeEnum.Ranking]: {
options: [
{
label: t("environments.surveys.edit.is_submitted"),
@@ -329,7 +329,7 @@ export const getLogicRules = (t: TFunction) => {
},
],
},
[TSurveyQuestionTypeEnum.Cal]: {
[TSurveyElementTypeEnum.Cal]: {
options: [
{
label: t("environments.surveys.edit.is_booked"),
@@ -341,7 +341,7 @@ export const getLogicRules = (t: TFunction) => {
},
],
},
[TSurveyQuestionTypeEnum.Matrix]: {
[TSurveyElementTypeEnum.Matrix]: {
options: [
{
label: t("environments.surveys.edit.is_partially_submitted"),
@@ -357,7 +357,7 @@ export const getLogicRules = (t: TFunction) => {
},
],
},
[`${TSurveyQuestionTypeEnum.Matrix}.row`]: {
[`${TSurveyElementTypeEnum.Matrix}.row`]: {
options: [
{
label: t("environments.surveys.edit.equals"),
@@ -382,7 +382,7 @@ export const getLogicRules = (t: TFunction) => {
},
],
},
[TSurveyQuestionTypeEnum.Address]: {
[TSurveyElementTypeEnum.Address]: {
options: [
{
label: t("environments.surveys.edit.is_submitted"),
@@ -394,7 +394,7 @@ export const getLogicRules = (t: TFunction) => {
},
],
},
[TSurveyQuestionTypeEnum.ContactInfo]: {
[TSurveyElementTypeEnum.ContactInfo]: {
options: [
{
label: t("environments.surveys.edit.is_submitted"),
@@ -518,6 +518,6 @@ export const getLogicRules = (t: TFunction) => {
};
};
export type TLogicRuleOption = ReturnType<typeof getLogicRules>["question"][keyof ReturnType<
export type TLogicRuleOption = ReturnType<typeof getLogicRules>["element"][keyof ReturnType<
typeof getLogicRules
>["question"]]["options"];
>["element"]]["options"];

View File

@@ -15,8 +15,6 @@ import {
import {
TSurvey,
TSurveyEndings,
TSurveyQuestion,
TSurveyQuestionId,
TSurveyVariable,
TSurveyWelcomeCard,
} from "@formbricks/types/surveys/types";
@@ -26,7 +24,7 @@ import { isConditionGroup } from "@/lib/surveyLogic/utils";
import { recallToHeadline } from "@/lib/utils/recall";
import { findElementLocation } from "@/modules/survey/editor/lib/blocks";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { getQuestionTypes, getTSurveyQuestionTypeEnumName } from "@/modules/survey/lib/questions";
import { getElementTypes, getTSurveyElementTypeEnumName } from "@/modules/survey/lib/elements";
import { TComboboxGroupedOption, TComboboxOption } from "@/modules/ui/components/input-combo-box";
import { TLogicRuleOption, getLogicRules } from "./logic-rule-engine";
@@ -97,8 +95,8 @@ export const formatTextWithSlashes = (
});
};
const getQuestionIconMapping = (t: TFunction) =>
getQuestionTypes(t).reduce(
const getElementIconMapping = (t: TFunction) =>
getElementTypes(t).reduce(
(prev, curr) => ({
...prev,
[curr.id]: curr.icon,
@@ -120,7 +118,7 @@ const getElementHeadline = (
return textContent;
}
}
return getTSurveyQuestionTypeEnumName(element.type, t) ?? "";
return getTSurveyElementTypeEnumName(element.type, t) ?? "";
};
export const getConditionValueOptions = (
@@ -154,7 +152,7 @@ export const getConditionValueOptions = (
const rows = element.rows.map((row, rowIdx) => {
const processedLabel = recallToHeadline(row.label, localSurvey, false, "default");
return {
icon: getQuestionIconMapping(t)[element.type],
icon: getElementIconMapping(t)[element.type],
label: `${getTextContent(processedLabel.default ?? "")} (${elementHeadline})`,
value: `${element.id}.${rowIdx}`,
meta: {
@@ -165,7 +163,7 @@ export const getConditionValueOptions = (
});
elementOptions.push({
icon: getQuestionIconMapping(t)[element.type],
icon: getElementIconMapping(t)[element.type],
label: elementHeadline,
value: element.id,
meta: {
@@ -188,7 +186,7 @@ export const getConditionValueOptions = (
});
} else {
elementOptions.push({
icon: getQuestionIconMapping(t)[element.type],
icon: getElementIconMapping(t)[element.type],
label: getElementHeadline(localSurvey, element, "default", t),
value: element.id,
meta: {
@@ -223,7 +221,7 @@ export const getConditionValueOptions = (
if (elementOptions.length > 0) {
groupedOptions.push({
label: t("common.questions"),
value: "questions",
value: "elements",
options: elementOptions,
});
}
@@ -276,13 +274,13 @@ export const getElementOperatorOptions = (
if (element.type === "openText") {
const inputType = element.inputType === "number" ? "number" : "text";
options = getLogicRules(t).question[`openText.${inputType}`].options;
options = getLogicRules(t).element[`openText.${inputType}`].options;
} else if (element.type === TSurveyElementTypeEnum.Matrix && condition) {
const isMatrixRow =
condition.leftOperand.type === "element" && condition.leftOperand?.meta?.row !== undefined;
options = getLogicRules(t).question[`matrix${isMatrixRow ? ".row" : ""}`].options;
options = getLogicRules(t).element[`matrix${isMatrixRow ? ".row" : ""}`].options;
} else {
options = getLogicRules(t).question[element.type].options;
options = getLogicRules(t).element[element.type].options;
}
if (element.required) {
@@ -303,9 +301,9 @@ export const getDefaultOperatorForElement = (
export const getFormatLeftOperandValue = (condition: TSingleCondition, localSurvey: TSurvey): string => {
if (condition.leftOperand.type === "element") {
const questions = getElementsFromBlocks(localSurvey.blocks);
const question = questions.find((q) => q.id === condition.leftOperand.value);
if (question && question.type === TSurveyElementTypeEnum.Matrix) {
const elements = getElementsFromBlocks(localSurvey.blocks);
const element = elements.find((e) => e.id === condition.leftOperand.value);
if (element && element.type === TSurveyElementTypeEnum.Matrix) {
if (condition.leftOperand?.meta?.row !== undefined) {
return `${condition.leftOperand.value}.${condition.leftOperand.meta.row}`;
}
@@ -327,14 +325,14 @@ export const getConditionOperatorOptions = (
} else if (condition.leftOperand.type === "hiddenField") {
return getLogicRules(t).hiddenField.options;
} else if (condition.leftOperand.type === "element") {
// Derive questions from blocks
// Derive elements from blocks
const elements = getElementsFromBlocks(localSurvey.blocks);
const element = elements.find((question) => {
let leftOperandQuestionId = condition.leftOperand.value;
if (question.type === TSurveyElementTypeEnum.Matrix) {
leftOperandQuestionId = condition.leftOperand.value.split(".")[0];
const element = elements.find((element) => {
let leftOperandElementId = condition.leftOperand.value;
if (element.type === TSurveyElementTypeEnum.Matrix) {
leftOperandElementId = condition.leftOperand.value.split(".")[0];
}
return question.id === leftOperandQuestionId;
return element.id === leftOperandElementId;
});
if (!element) return [];
@@ -419,7 +417,7 @@ export const getMatchValueProps = (
const elementOptions = allowedElements.map((element) => {
return {
icon: getQuestionIconMapping(t)[element.type],
icon: getElementIconMapping(t)[element.type],
label: getTextContent(
recallToHeadline(element.headline, localSurvey, false, "default").default ?? ""
),
@@ -461,7 +459,7 @@ export const getMatchValueProps = (
if (elementOptions.length > 0) {
groupedOptions.push({
label: t("common.questions"),
value: "questions",
value: "elements",
options: elementOptions,
});
}
@@ -636,7 +634,7 @@ export const getMatchValueProps = (
const elementOptions = openTextElements.map((element) => {
return {
icon: getQuestionIconMapping(t)[element.type],
icon: getElementIconMapping(t)[element.type],
label: getTextContent(
recallToHeadline(element.headline, localSurvey, false, "default").default ?? ""
),
@@ -676,7 +674,7 @@ export const getMatchValueProps = (
if (elementOptions.length > 0) {
groupedOptions.push({
label: t("common.questions"),
value: "questions",
value: "elements",
options: elementOptions,
});
}
@@ -735,7 +733,7 @@ export const getMatchValueProps = (
const elementOptions = allowedElements.map((element) => {
return {
icon: getQuestionIconMapping(t)[element.type],
icon: getElementIconMapping(t)[element.type],
label: getElementHeadline(localSurvey, element, "default", t),
value: element.id,
meta: {
@@ -773,7 +771,7 @@ export const getMatchValueProps = (
if (elementOptions.length > 0) {
groupedOptions.push({
label: t("common.questions"),
value: "questions",
value: "elements",
options: elementOptions,
});
}
@@ -809,7 +807,7 @@ export const getMatchValueProps = (
const elementOptions = allowedElements.map((element) => {
return {
icon: getQuestionIconMapping(t)[element.type],
icon: getElementIconMapping(t)[element.type],
label: getElementHeadline(localSurvey, element, "default", t),
value: element.id,
meta: {
@@ -847,7 +845,7 @@ export const getMatchValueProps = (
if (elementOptions.length > 0) {
groupedOptions.push({
label: t("common.questions"),
value: "questions",
value: "elements",
options: elementOptions,
});
}
@@ -889,7 +887,7 @@ export const getMatchValueProps = (
const elementOptions = allowedElements.map((element) => {
return {
icon: getQuestionIconMapping(t)[element.type],
icon: getElementIconMapping(t)[element.type],
label: getElementHeadline(localSurvey, element, "default", t),
value: element.id,
meta: {
@@ -927,7 +925,7 @@ export const getMatchValueProps = (
if (elementOptions.length > 0) {
groupedOptions.push({
label: t("common.questions"),
value: "questions",
value: "elements",
options: elementOptions,
});
}
@@ -984,7 +982,7 @@ export const getActionTargetOptions = (
// Return element IDs for requireAnswer
return nonRequiredElements.map((element) => {
return {
icon: getQuestionIconMapping(t)[element.type],
icon: getElementIconMapping(t)[element.type],
label: getElementHeadline(localSurvey, element, "default", t),
value: element.id,
};
@@ -1130,7 +1128,7 @@ export const getActionValueOptions = (
const elementOptions = allowedElements.map((element) => {
return {
icon: getQuestionIconMapping(t)[element.type],
icon: getElementIconMapping(t)[element.type],
label: getElementHeadline(localSurvey, element, "default", t),
value: element.id,
meta: {
@@ -1157,7 +1155,7 @@ export const getActionValueOptions = (
if (elementOptions.length > 0) {
groupedOptions.push({
label: t("common.questions"),
value: "questions",
value: "elements",
options: elementOptions,
});
}
@@ -1188,7 +1186,7 @@ export const getActionValueOptions = (
const elementOptions = allowedElements.map((element) => {
return {
icon: getQuestionIconMapping(t)[element.type],
icon: getElementIconMapping(t)[element.type],
label: getTextContent(getLocalizedValue(element.headline, "default")),
value: element.id,
meta: {
@@ -1215,7 +1213,7 @@ export const getActionValueOptions = (
if (elementOptions.length > 0) {
groupedOptions.push({
label: t("common.questions"),
value: "questions",
value: "elements",
options: elementOptions,
});
}
@@ -1276,10 +1274,10 @@ const isUsedInRightOperand = (
}
};
export const findQuestionUsedInLogic = (survey: TSurvey, questionId: TSurveyQuestionId): number => {
const { block } = findElementLocation(survey, questionId);
export const findElementUsedInLogic = (survey: TSurvey, elementId: string): number => {
const { block } = findElementLocation(survey, elementId);
// The parent block for this questionId was not found in the survey, while this shouldn't happen but we still have a safety check and return -1
// The parent block for this elementId was not found in the survey, while this shouldn't happen but we still have a safety check and return -1
if (!block) {
return -1;
}
@@ -1291,14 +1289,14 @@ export const findQuestionUsedInLogic = (survey: TSurvey, questionId: TSurveyQues
} else {
// It's a TSingleCondition
return (
(condition.rightOperand && isUsedInRightOperand(condition.rightOperand, "element", questionId)) ||
isUsedInLeftOperand(condition.leftOperand, "element", questionId)
(condition.rightOperand && isUsedInRightOperand(condition.rightOperand, "element", elementId)) ||
isUsedInLeftOperand(condition.leftOperand, "element", elementId)
);
}
};
const isUsedInAction = (action: TSurveyBlockLogicAction): boolean => {
if (action.objective === "requireAnswer" && action.target === questionId) {
if (action.objective === "requireAnswer" && action.target === elementId) {
return true;
}
@@ -1309,19 +1307,17 @@ export const findQuestionUsedInLogic = (survey: TSurvey, questionId: TSurveyQues
return isUsedInCondition(logicRule.conditions) || logicRule.actions.some(isUsedInAction);
};
// Derive questions from blocks (cast as questions to access logic properties)
const questions = getElementsFromBlocks(survey.blocks);
const elements = getElementsFromBlocks(survey.blocks);
return questions.findIndex((question) => {
const { block } = findElementLocation(survey, question.id);
return elements.findIndex((element) => {
const { block } = findElementLocation(survey, element.id);
if (!block) {
return false;
}
return (
block.logicFallback === questionId ||
(question.id !== questionId && block.logic?.some(isUsedInLogicRule))
block.logicFallback === elementId || (element.id !== elementId && block.logic?.some(isUsedInLogicRule))
);
});
};
@@ -1329,22 +1325,22 @@ export const findQuestionUsedInLogic = (survey: TSurvey, questionId: TSurveyQues
export const isUsedInQuota = (
quota: TSurveyQuota,
{
questionId,
elementId,
hiddenFieldId,
variableId,
endingCardId,
}: {
questionId?: TSurveyQuestionId;
elementId?: string;
hiddenFieldId?: string;
variableId?: string;
endingCardId?: string;
}
): boolean => {
if (questionId) {
if (elementId) {
return quota.logic.conditions.some(
(condition) =>
(condition.rightOperand && isUsedInRightOperand(condition.rightOperand, "element", questionId)) ||
isUsedInLeftOperand(condition.leftOperand, "element", questionId)
(condition.rightOperand && isUsedInRightOperand(condition.rightOperand, "element", elementId)) ||
isUsedInLeftOperand(condition.leftOperand, "element", elementId)
);
}
@@ -1385,14 +1381,14 @@ const checkWelcomeCardForRecall = (welcomeCard: TSurveyWelcomeCard, recallPatter
);
};
const checkQuestionForRecall = (question: TSurveyQuestion, recallPattern: string): boolean => {
const checkElementForRecall = (element: TSurveyElement, recallPattern: string): boolean => {
// Check headline
if (Object.values(question.headline).some((text) => text.includes(recallPattern))) {
if (Object.values(element.headline).some((text) => text.includes(recallPattern))) {
return true;
}
// Check subheader
if (checkTextForRecallPattern(question.subheader, recallPattern)) {
if (checkTextForRecallPattern(element.subheader, recallPattern)) {
return true;
}
@@ -1421,18 +1417,16 @@ export const isUsedInRecall = (survey: TSurvey, id: string): number => {
return -2; // Special index for welcome card
}
// Derive questions from blocks (cast as questions to access logic properties)
const questions = (survey.blocks?.flatMap((b) => b.elements) ?? []) as unknown as TSurveyQuestion[];
const elements = getElementsFromBlocks(survey.blocks);
// Check questions
const questionIndex = questions.findIndex((question) => checkQuestionForRecall(question, recallPattern));
if (questionIndex !== -1) {
return questionIndex;
const elementIndex = elements.findIndex((element) => checkElementForRecall(element, recallPattern));
if (elementIndex !== -1) {
return elementIndex;
}
// Check ending cards
if (checkEndingCardsForRecall(survey.endings, recallPattern)) {
return questions.length; // Special index for ending cards
return elements.length; // Special index for ending cards
}
return -1; // Not found
@@ -1440,7 +1434,7 @@ export const isUsedInRecall = (survey: TSurvey, id: string): number => {
export const findOptionUsedInLogic = (
survey: TSurvey,
questionId: TSurveyQuestionId,
elementId: string,
optionId: string,
checkInLeftOperand: boolean = false
): number => {
@@ -1455,7 +1449,7 @@ export const findOptionUsedInLogic = (
};
const isUsedInOperand = (condition: TSingleCondition): boolean => {
if (condition.leftOperand.type === "element" && condition.leftOperand.value === questionId) {
if (condition.leftOperand.type === "element" && condition.leftOperand.value === elementId) {
if (checkInLeftOperand) {
if (condition.leftOperand.meta && Object.entries(condition.leftOperand.meta).length > 0) {
const optionIdInMeta = Object.values(condition.leftOperand.meta).some(
@@ -1479,11 +1473,10 @@ export const findOptionUsedInLogic = (
return isUsedInCondition(logicRule.conditions);
};
// Derive questions from blocks (cast as questions to access logic properties)
const questions = getElementsFromBlocks(survey.blocks);
const elements = getElementsFromBlocks(survey.blocks);
return questions.findIndex((question) => {
const { block } = findElementLocation(survey, question.id);
return elements.findIndex((element) => {
const { block } = findElementLocation(survey, element.id);
if (!block) {
return false;
@@ -1515,11 +1508,10 @@ export const findVariableUsedInLogic = (survey: TSurvey, variableId: string): nu
return isUsedInCondition(logicRule.conditions) || logicRule.actions.some(isUsedInAction);
};
// Derive questions from blocks (cast as questions to access logic properties)
const questions = (survey.blocks?.flatMap((b) => b.elements) ?? []) as unknown as TSurveyQuestion[];
const elements = survey.blocks.flatMap((b) => b.elements);
return questions.findIndex((question) => {
const { block } = findElementLocation(survey, question.id);
return elements.findIndex((element) => {
const { block } = findElementLocation(survey, element.id);
if (!block) {
return false;
@@ -1548,11 +1540,10 @@ export const findHiddenFieldUsedInLogic = (survey: TSurvey, hiddenFieldId: strin
return isUsedInCondition(logicRule.conditions);
};
// Derive questions from blocks (cast as questions to access logic properties)
const questions = getElementsFromBlocks(survey.blocks);
const elements = getElementsFromBlocks(survey.blocks);
return questions.findIndex((question) => {
const { block } = findElementLocation(survey, question.id);
return elements.findIndex((element) => {
const { block } = findElementLocation(survey, element.id);
if (!block) {
return false;
@@ -1578,11 +1569,10 @@ export const findEndingCardUsedInLogic = (survey: TSurvey, endingCardId: string)
return logicRule.actions.some(isUsedInAction);
};
// Derive questions from blocks (cast as questions to access logic properties)
const questions = getElementsFromBlocks(survey.blocks);
const elements = getElementsFromBlocks(survey.blocks);
return questions.findIndex((question) => {
const { block } = findElementLocation(survey, question.id);
return elements.findIndex((element) => {
const { block } = findElementLocation(survey, element.id);
if (!block) {
return false;

View File

@@ -17,7 +17,7 @@ import { TSurveyFollowUp } from "@formbricks/database/types/survey-follow-up";
import { TResponse } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys/types";
import { FB_LOGO_URL, IMPRINT_ADDRESS, IMPRINT_URL, PRIVACY_URL } from "@/lib/constants";
import { getQuestionResponseMapping } from "@/lib/responses";
import { getElementResponseMapping } from "@/lib/responses";
import { getTranslate } from "@/lingodotdev/server";
import { renderEmailResponseValue } from "@/modules/email/emails/lib/utils";
@@ -36,7 +36,7 @@ export async function FollowUpEmail(props: FollowUpEmailProps): Promise<React.JS
const { properties } = props.followUp.action;
const { body } = properties;
const questions = props.attachResponseData ? getQuestionResponseMapping(props.survey, props.response) : [];
const questions = props.attachResponseData ? getElementResponseMapping(props.survey, props.response) : [];
const t = await getTranslate();
// If the logo is not set, we are not using white labeling
const isDefaultLogo = !props.logoUrl || props.logoUrl === fbLogoUrl;
@@ -75,9 +75,9 @@ export async function FollowUpEmail(props: FollowUpEmailProps): Promise<React.JS
{questions.map((question) => {
if (!question.response) return;
return (
<Row key={question.question}>
<Row key={question.element}>
<Column className="w-full font-medium">
<Text className="mb-2 text-sm">{question.question}</Text>
<Text className="mb-2 text-sm">{question.element}</Text>
{renderEmailResponseValue(question.response, question.type, t, true)}
</Column>
</Row>

View File

@@ -30,7 +30,7 @@ import {
} from "@/modules/survey/editor/types/survey-follow-up";
import FollowUpActionMultiEmailInput from "@/modules/survey/follow-ups/components/follow-up-action-multi-email-input";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { getQuestionIconMap } from "@/modules/survey/lib/questions";
import { getElementIconMap } from "@/modules/survey/lib/elements";
import { Alert, AlertTitle } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button";
import { Checkbox } from "@/modules/ui/components/checkbox";
@@ -97,7 +97,7 @@ export const FollowUpModal = ({
locale,
}: AddFollowUpModalProps) => {
const { t } = useTranslation();
const QUESTIONS_ICON_MAP = getQuestionIconMap(t);
const QUESTIONS_ICON_MAP = getElementIconMap(t);
const containerRef = useRef<HTMLDivElement>(null);
const [firstRender, setFirstRender] = useState(true);

Some files were not shown because too many files have changed in this diff Show More