mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 13:49:54 -06:00
code cleanup
This commit is contained in:
@@ -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>({
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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")}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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")}
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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 })}`]
|
||||
)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
@@ -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">
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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"> = {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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>
|
||||
@@ -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} />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) => {
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
@@ -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";
|
||||
@@ -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>
|
||||
))}
|
||||
@@ -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>
|
||||
))}
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
@@ -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")}
|
||||
@@ -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>
|
||||
@@ -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 (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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")}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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()}>
|
||||
@@ -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([
|
||||
{
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user