From b2b97c8bedfed61f41937df0903545bc54396ac5 Mon Sep 17 00:00:00 2001 From: pandeymangg Date: Thu, 6 Nov 2025 12:02:25 +0530 Subject: [PATCH] fixes feedback comments --- apps/web/lib/utils/recall.ts | 9 +- .../components/localized-editor.tsx | 6 +- .../components/secondary-language-select.tsx | 6 +- .../ee/quotas/components/quota-modal.tsx | 5 +- .../components/preview-email-template.tsx | 24 ++-- .../components/recall-item-select.tsx | 10 +- .../components/recall-wrapper.tsx | 3 +- .../components/question-form-input/index.tsx | 6 +- .../question-form-input/utils.test.ts | 48 +++----- .../components/question-form-input/utils.ts | 3 +- .../editor/components/conditional-logic.tsx | 7 +- .../editor/components/editor-card-menu.tsx | 4 +- .../editor/components/end-screen-form.tsx | 3 +- .../editor/components/hidden-fields-card.tsx | 11 +- .../survey/editor/components/logic-editor.tsx | 3 +- .../editor/components/questions-droppable.tsx | 5 +- .../editor/components/questions-view.tsx | 38 ++----- .../components/survey-variables-card-item.tsx | 3 +- .../editor/components/update-question-id.tsx | 3 +- apps/web/modules/survey/editor/lib/blocks.ts | 49 +++++---- .../editor/lib/shared-conditions-factory.ts | 3 +- apps/web/modules/survey/editor/lib/utils.tsx | 103 ++++++++++++------ .../follow-ups/components/follow-up-modal.tsx | 3 +- 23 files changed, 180 insertions(+), 175 deletions(-) diff --git a/apps/web/lib/utils/recall.ts b/apps/web/lib/utils/recall.ts index 55a11c7011..5e8f0d6cdc 100644 --- a/apps/web/lib/utils/recall.ts +++ b/apps/web/lib/utils/recall.ts @@ -5,6 +5,7 @@ import { TSurvey, TSurveyRecallItem } from "@formbricks/types/surveys/types"; import { getTextContent } from "@formbricks/types/surveys/validation"; import { getLocalizedValue } from "@/lib/i18n/utils"; import { structuredClone } from "@/lib/pollyfills/structuredClone"; +import { getQuestionsFromBlocks } from "@/modules/survey/editor/lib/blocks"; import { formatDateWithOrdinal, isValidDateString } from "./datetime"; export interface fallbacks { @@ -61,7 +62,7 @@ const getRecallItemLabel = ( const isHiddenField = survey.hiddenFields.fieldIds?.includes(recallItemId); if (isHiddenField) return recallItemId; - const questions = survey.blocks.flatMap((b) => b.elements); + const questions = getQuestionsFromBlocks(survey.blocks); const surveyQuestion = questions.find((question) => question.id === recallItemId); if (surveyQuestion) { const headline = getLocalizedValue(surveyQuestion.headline, languageCode); @@ -131,7 +132,7 @@ export const checkForEmptyFallBackValue = (survey: TSurvey, language: string): T return recalls?.some((recall) => !extractFallbackValue(recall)); }; - const questions = survey.blocks.flatMap((b) => b.elements); + const questions = getQuestionsFromBlocks(survey.blocks); for (const question of questions) { if ( doesTextHaveRecall(getLocalizedValue(question.headline, language)) || @@ -146,7 +147,7 @@ export const checkForEmptyFallBackValue = (survey: TSurvey, language: string): T // Processes each question in a survey to ensure headlines are formatted correctly for recall and return the modified survey. export const replaceHeadlineRecall = (survey: T, language: string): T => { const modifiedSurvey = structuredClone(survey); - const questions = modifiedSurvey.blocks.flatMap((b) => b.elements); + const questions = getQuestionsFromBlocks(modifiedSurvey.blocks); questions.forEach((question) => { question.headline = recallToHeadline(question.headline, modifiedSurvey, false, language); }); @@ -161,7 +162,7 @@ export const getRecallItems = (text: string, survey: TSurvey, languageCode: stri let recallItems: TSurveyRecallItem[] = []; ids.forEach((recallItemId) => { const isHiddenField = survey.hiddenFields.fieldIds?.includes(recallItemId); - const questions = survey.blocks.flatMap((b) => b.elements); + const questions = getQuestionsFromBlocks(survey.blocks); const isSurveyQuestion = questions.find((question) => question.id === recallItemId); const isVariable = survey.variables.find((variable) => variable.id === recallItemId); diff --git a/apps/web/modules/ee/multi-language-surveys/components/localized-editor.tsx b/apps/web/modules/ee/multi-language-surveys/components/localized-editor.tsx index e4f6270118..bea4f9bf71 100644 --- a/apps/web/modules/ee/multi-language-surveys/components/localized-editor.tsx +++ b/apps/web/modules/ee/multi-language-surveys/components/localized-editor.tsx @@ -9,6 +9,7 @@ import { getTextContent, isValidHTML } from "@formbricks/types/surveys/validatio import { TUserLocale } from "@formbricks/types/user"; import { md } from "@/lib/markdownIt"; import { recallToHeadline } from "@/lib/utils/recall"; +import { getQuestionsFromBlocks } from "@/modules/survey/editor/lib/blocks"; import { isLabelValidForAllLanguages } from "@/modules/survey/editor/lib/validation"; import { Editor } from "@/modules/ui/components/editor"; import { LanguageIndicator } from "./language-indicator"; @@ -63,10 +64,7 @@ export function LocalizedEditor({ isExternalUrlsAllowed, }: Readonly) { // Derive questions from blocks for migrated surveys - const questions = useMemo( - () => localSurvey.blocks.flatMap((block) => block.elements), - [localSurvey.blocks] - ); + const questions = useMemo(() => getQuestionsFromBlocks(localSurvey.blocks), [localSurvey.blocks]); const { t } = useTranslation(); const isInComplete = useMemo( diff --git a/apps/web/modules/ee/multi-language-surveys/components/secondary-language-select.tsx b/apps/web/modules/ee/multi-language-surveys/components/secondary-language-select.tsx index e74f89383f..04651e68f4 100644 --- a/apps/web/modules/ee/multi-language-surveys/components/secondary-language-select.tsx +++ b/apps/web/modules/ee/multi-language-surveys/components/secondary-language-select.tsx @@ -1,10 +1,10 @@ "use client"; import { Language } from "@prisma/client"; -import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import type { TSurvey } from "@formbricks/types/surveys/types"; import { TUserLocale } from "@formbricks/types/user"; +import { getQuestionsFromBlocks } from "@/modules/survey/editor/lib/blocks"; import { LanguageToggle } from "./language-toggle"; interface SecondaryLanguageSelectProps { @@ -33,9 +33,7 @@ export function SecondaryLanguageSelect({ ); }; - const questions = useMemo(() => { - return localSurvey.blocks.flatMap((block) => block.elements); - }, [localSurvey.blocks]); + const questions = getQuestionsFromBlocks(localSurvey.blocks); return (
diff --git a/apps/web/modules/ee/quotas/components/quota-modal.tsx b/apps/web/modules/ee/quotas/components/quota-modal.tsx index 647a0f8d7a..5ba3dab30d 100644 --- a/apps/web/modules/ee/quotas/components/quota-modal.tsx +++ b/apps/web/modules/ee/quotas/components/quota-modal.tsx @@ -20,6 +20,7 @@ import { TSurvey } from "@formbricks/types/surveys/types"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { createQuotaAction, updateQuotaAction } from "@/modules/ee/quotas/actions"; import { EndingCardSelector } from "@/modules/ee/quotas/components/ending-card-selector"; +import { getQuestionsFromBlocks } from "@/modules/survey/editor/lib/blocks"; import { getDefaultOperatorForQuestion } from "@/modules/survey/editor/lib/utils"; import { Button } from "@/modules/ui/components/button"; import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal"; @@ -81,9 +82,7 @@ export const QuotaModal = ({ const [openConfirmationModal, setOpenConfirmationModal] = useState(false); const [openConfirmChangesInInclusionCriteria, setOpenConfirmChangesInInclusionCriteria] = useState(false); - const questions = useMemo(() => { - return survey.blocks.flatMap((block) => block.elements); - }, [survey.blocks]); + const questions = useMemo(() => getQuestionsFromBlocks(survey.blocks), [survey.blocks]); const defaultValues = useMemo(() => { const firstQuestion = questions[0]; diff --git a/apps/web/modules/email/components/preview-email-template.tsx b/apps/web/modules/email/components/preview-email-template.tsx index 77acecbddf..e2360108f4 100644 --- a/apps/web/modules/email/components/preview-email-template.tsx +++ b/apps/web/modules/email/components/preview-email-template.tsx @@ -13,7 +13,7 @@ import { render } from "@react-email/render"; import { TFunction } from "i18next"; import { CalendarDaysIcon, UploadIcon } from "lucide-react"; import React from "react"; -import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements"; +import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements"; import { type TSurvey, type TSurveyStyling } from "@formbricks/types/surveys/types"; import { cn } from "@/lib/cn"; import { WEBAPP_URL } from "@/lib/constants"; @@ -22,6 +22,7 @@ import { COLOR_DEFAULTS } from "@/lib/styling/constants"; import { isLight, mixColor } from "@/lib/utils/colors"; import { parseRecallInfo } from "@/lib/utils/recall"; import { RatingSmiley } from "@/modules/analysis/components/RatingSmiley"; +import { findElementLocation, getQuestionsFromBlocks } from "@/modules/survey/editor/lib/blocks"; import { getNPSOptionColor, getRatingNumberOptionColor } from "../lib/utils"; import { QuestionHeader } from "./email-question-header"; @@ -78,24 +79,17 @@ export async function PreviewEmailTemplate({ const url = `${surveyUrl}?preview=true`; const urlWithPrefilling = `${surveyUrl}?preview=true&skipPrefilled=true&`; const defaultLanguageCode = "default"; + // Derive questions from blocks - const questions = survey.blocks.flatMap((block) => block.elements); - const firstQuestion = questions[0] as TSurveyElement; + const questions = getQuestionsFromBlocks(survey.blocks); + const firstQuestion = questions[0]; + + const { block } = findElementLocation(survey, firstQuestion.id); + const headline = parseRecallInfo(getLocalizedValue(firstQuestion.headline, defaultLanguageCode)); const subheader = parseRecallInfo(getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)); const brandColor = styling.brandColor?.light ?? COLOR_DEFAULTS.brandColor; - const getButtonLabel = (survey: TSurvey, defaultLanguageCode: string) => { - const ctaQuestionBlock = survey.blocks.find((block) => - block.elements.some((element) => element.type === TSurveyElementTypeEnum.CTA) - ); - if (ctaQuestionBlock) { - return getLocalizedValue(ctaQuestionBlock.buttonLabel, defaultLanguageCode); - } - - return t("common.next"); - }; - switch (firstQuestion.type) { case TSurveyElementTypeEnum.OpenText: return ( @@ -201,7 +195,7 @@ export async function PreviewEmailTemplate({ isLight(brandColor) ? "text-black" : "text-white" )} href={`${urlWithPrefilling}${firstQuestion.id}=clicked`}> - {getButtonLabel(survey, defaultLanguageCode)} + {getLocalizedValue(block?.buttonLabel, defaultLanguageCode)} diff --git a/apps/web/modules/survey/components/question-form-input/components/recall-item-select.tsx b/apps/web/modules/survey/components/question-form-input/components/recall-item-select.tsx index ff325ba4d7..ead37479a0 100644 --- a/apps/web/modules/survey/components/question-form-input/components/recall-item-select.tsx +++ b/apps/web/modules/survey/components/question-form-input/components/recall-item-select.tsx @@ -18,6 +18,7 @@ import { useTranslation } from "react-i18next"; import { TSurveyElement, TSurveyElementId, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements"; import { TSurvey, TSurveyHiddenFields, TSurveyRecallItem } from "@formbricks/types/surveys/types"; import { getTextContentWithRecallTruncated } from "@/lib/utils/recall"; +import { getQuestionsFromBlocks } from "@/modules/survey/editor/lib/blocks"; import { DropdownMenu, DropdownMenuContent, @@ -70,6 +71,8 @@ export const RecallItemSelect = ({ ); }; + const questions = useMemo(() => getQuestionsFromBlocks(localSurvey.blocks), [localSurvey.blocks]); + const recallItemIds = useMemo(() => { return recallItems.map((recallItem) => recallItem.id); }, [recallItems]); @@ -109,9 +112,6 @@ export const RecallItemSelect = ({ const isWelcomeCard = questionId === "start"; if (isWelcomeCard) return []; - // Derive questions from blocks or fall back to legacy questions array - const questions = localSurvey.blocks.flatMap((block) => block.elements); - const isEndingCard = !questions.map((question) => question.id).includes(questionId); const idx = isEndingCard ? questions.length @@ -128,7 +128,7 @@ export const RecallItemSelect = ({ }); return filteredQuestions; - }, [localSurvey.blocks, questionId, recallItemIds, selectedLanguageCode]); + }, [questionId, questions, recallItemIds, selectedLanguageCode]); const filteredRecallItems: TSurveyRecallItem[] = useMemo(() => { return [...surveyQuestionRecallItems, ...hiddenFieldRecallItems, ...variableRecallItems].filter( @@ -144,8 +144,6 @@ export const RecallItemSelect = ({ const getRecallItemIcon = (recallItem: TSurveyRecallItem) => { switch (recallItem.type) { case "question": - // Derive questions from blocks or fall back to legacy questions array - const questions = localSurvey.blocks.flatMap((block) => block.elements); const question = questions.find((question) => question.id === recallItem.id); if (question) { return questionIconMapping[question?.type as keyof typeof questionIconMapping]; diff --git a/apps/web/modules/survey/components/question-form-input/components/recall-wrapper.tsx b/apps/web/modules/survey/components/question-form-input/components/recall-wrapper.tsx index 25c556b1a0..17ee872521 100644 --- a/apps/web/modules/survey/components/question-form-input/components/recall-wrapper.tsx +++ b/apps/web/modules/survey/components/question-form-input/components/recall-wrapper.tsx @@ -18,6 +18,7 @@ import { } 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 { getQuestionsFromBlocks } from "@/modules/survey/editor/lib/blocks"; import { Button } from "@/modules/ui/components/button"; interface RecallWrapperRenderProps { @@ -189,7 +190,7 @@ export const RecallWrapper = ({ const info = extractRecallInfo(recallItem.label); if (info) { const recallItemId = extractId(info); - const questions = localSurvey.blocks.flatMap((block) => block.elements); + const questions = getQuestionsFromBlocks(localSurvey.blocks); const recallQuestion = questions.find((q) => q.id === recallItemId); if (recallQuestion) { // replace nested recall with "___" diff --git a/apps/web/modules/survey/components/question-form-input/index.tsx b/apps/web/modules/survey/components/question-form-input/index.tsx index 45c176877e..f8e6c9b00f 100644 --- a/apps/web/modules/survey/components/question-form-input/index.tsx +++ b/apps/web/modules/survey/components/question-form-input/index.tsx @@ -21,6 +21,7 @@ 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 { getQuestionsFromBlocks } from "@/modules/survey/editor/lib/blocks"; import { Button } from "@/modules/ui/components/button"; import { FileInput } from "@/modules/ui/components/file-input"; import { Input } from "@/modules/ui/components/input"; @@ -92,7 +93,8 @@ export const QuestionFormInput = ({ const defaultLanguageCode = localSurvey.languages.filter((lang) => lang.default)[0]?.language.code ?? "default"; const usedLanguageCode = selectedLanguageCode === defaultLanguageCode ? "default" : selectedLanguageCode; - const questions = localSurvey.blocks.flatMap((block) => block.elements); + + const questions = useMemo(() => getQuestionsFromBlocks(localSurvey.blocks), [localSurvey.blocks]); const question: TSurveyElement = questions[questionIdx]; const isChoice = id.includes("choice"); @@ -168,7 +170,7 @@ export const QuestionFormInput = ({ const [text, setText] = useState(elementText); const [showImageUploader, setShowImageUploader] = useState( - determineImageUploaderVisibility(questionIdx, localSurvey) + determineImageUploaderVisibility(questionIdx, questions) ); const highlightContainerRef = useRef(null); diff --git a/apps/web/modules/survey/components/question-form-input/utils.test.ts b/apps/web/modules/survey/components/question-form-input/utils.test.ts index edf7bd9e2a..7a72ee5b65 100644 --- a/apps/web/modules/survey/components/question-form-input/utils.test.ts +++ b/apps/web/modules/survey/components/question-form-input/utils.test.ts @@ -2,12 +2,8 @@ import "@testing-library/jest-dom/vitest"; import { TFunction } from "i18next"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { type TI18nString } from "@formbricks/types/i18n"; -import { - TSurvey, - TSurveyMultipleChoiceQuestion, - TSurveyQuestion, - TSurveyQuestionTypeEnum, -} from "@formbricks/types/surveys/types"; +import { TSurveyElement } from "@formbricks/types/surveys/elements"; +import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; import { createI18nString } from "@/lib/i18n/utils"; import * as i18nUtils from "@/lib/i18n/utils"; import { @@ -48,7 +44,7 @@ describe("utils", () => { describe("getChoiceLabel", () => { test("returns the choice label from a question", () => { const surveyLanguageCodes = ["en"]; - const choiceQuestion: TSurveyMultipleChoiceQuestion = { + const choiceQuestion = { id: "q1", type: TSurveyQuestionTypeEnum.MultipleChoiceMulti, headline: createI18nString("Question?", surveyLanguageCodes), @@ -57,7 +53,7 @@ describe("utils", () => { { id: "c1", label: createI18nString("Choice 1", surveyLanguageCodes) }, { id: "c2", label: createI18nString("Choice 2", surveyLanguageCodes) }, ], - }; + } as unknown as TSurveyElement; const result = getChoiceLabel(choiceQuestion, 1, surveyLanguageCodes); expect(result).toEqual(createI18nString("Choice 2", surveyLanguageCodes)); @@ -65,13 +61,13 @@ describe("utils", () => { test("returns empty i18n string when choice doesn't exist", () => { const surveyLanguageCodes = ["en"]; - const choiceQuestion: TSurveyMultipleChoiceQuestion = { + const choiceQuestion = { id: "q1", type: TSurveyQuestionTypeEnum.MultipleChoiceMulti, headline: createI18nString("Question?", surveyLanguageCodes), required: true, choices: [], - }; + } as unknown as TSurveyElement; const result = getChoiceLabel(choiceQuestion, 0, surveyLanguageCodes); expect(result).toEqual(createI18nString("", surveyLanguageCodes)); @@ -94,7 +90,7 @@ describe("utils", () => { { id: "col-1", label: createI18nString("Column 1", surveyLanguageCodes) }, { id: "col-2", label: createI18nString("Column 2", surveyLanguageCodes) }, ], - } as unknown as TSurveyQuestion; + } as unknown as TSurveyElement; const result = getMatrixLabel(matrixQuestion, 1, surveyLanguageCodes, "row"); expect(result).toEqual(createI18nString("Row 2", surveyLanguageCodes)); @@ -115,7 +111,7 @@ describe("utils", () => { { id: "col-1", label: createI18nString("Column 1", surveyLanguageCodes) }, { id: "col-2", label: createI18nString("Column 2", surveyLanguageCodes) }, ], - } as unknown as TSurveyQuestion; + } as unknown as TSurveyElement; const result = getMatrixLabel(matrixQuestion, 0, surveyLanguageCodes, "column"); expect(result).toEqual(createI18nString("Column 1", surveyLanguageCodes)); @@ -130,7 +126,7 @@ describe("utils", () => { required: true, rows: [], columns: [], - } as unknown as TSurveyQuestion; + } as unknown as TSurveyElement; const result = getMatrixLabel(matrixQuestion, 0, surveyLanguageCodes, "row"); expect(result).toEqual(createI18nString("", surveyLanguageCodes)); @@ -264,25 +260,7 @@ describe("utils", () => { describe("determineImageUploaderVisibility", () => { test("returns false for welcome card", () => { - const survey = { - id: "survey1", - name: "Test Survey", - createdAt: new Date(), - updatedAt: new Date(), - status: "draft", - questions: [], - welcomeCard: { enabled: true } as unknown as TSurvey["welcomeCard"], - styling: {}, - environmentId: "env1", - type: "app", - triggers: [], - recontactDays: null, - endings: [], - delay: 0, - pin: null, - } as unknown as TSurvey; - - const result = determineImageUploaderVisibility(-1, survey); + const result = determineImageUploaderVisibility(-1, []); expect(result).toBe(false); }); @@ -319,7 +297,7 @@ describe("utils", () => { pin: null, } as unknown as TSurvey; - const result = determineImageUploaderVisibility(0, survey); + const result = determineImageUploaderVisibility(0, survey.blocks[0].elements); expect(result).toBe(true); }); @@ -356,7 +334,7 @@ describe("utils", () => { pin: null, } as unknown as TSurvey; - const result = determineImageUploaderVisibility(0, survey); + const result = determineImageUploaderVisibility(0, survey.blocks[0].elements); expect(result).toBe(true); }); @@ -392,7 +370,7 @@ describe("utils", () => { pin: null, } as unknown as TSurvey; - const result = determineImageUploaderVisibility(0, survey); + const result = determineImageUploaderVisibility(0, survey.blocks[0].elements); expect(result).toBe(false); }); }); diff --git a/apps/web/modules/survey/components/question-form-input/utils.ts b/apps/web/modules/survey/components/question-form-input/utils.ts index 862131b0da..1246183e03 100644 --- a/apps/web/modules/survey/components/question-form-input/utils.ts +++ b/apps/web/modules/survey/components/question-form-input/utils.ts @@ -66,13 +66,12 @@ export const getEndingCardText = ( } }; -export const determineImageUploaderVisibility = (questionIdx: number, localSurvey: TSurvey) => { +export const determineImageUploaderVisibility = (questionIdx: number, questions: TSurveyElement[]) => { switch (questionIdx) { case -1: // Welcome Card return false; default: { // Regular Survey Question - derive questions from blocks - const questions = localSurvey.blocks.flatMap((block) => block.elements); const question = questions[questionIdx]; return (!!question && !!question.imageUrl) || (!!question && !!question.videoUrl); } diff --git a/apps/web/modules/survey/editor/components/conditional-logic.tsx b/apps/web/modules/survey/editor/components/conditional-logic.tsx index e2d92e46a0..ba95e723ee 100644 --- a/apps/web/modules/survey/editor/components/conditional-logic.tsx +++ b/apps/web/modules/survey/editor/components/conditional-logic.tsx @@ -58,8 +58,9 @@ export function ConditionalLogic({ }, [localSurvey]); // Find the parent block for this question/element to get its logic - const parentBlock = localSurvey.blocks.find((block) => - block.elements.some((element) => element.id === question.id) + const parentBlock = useMemo( + () => localSurvey.blocks.find((block) => block.elements.some((element) => element.id === question.id)), + [localSurvey.blocks, question.id] ); const blockLogic = useMemo(() => parentBlock?.logic ?? [], [parentBlock?.logic]); @@ -146,7 +147,7 @@ export function ConditionalLogic({ className="relative flex w-full grow items-start gap-2 rounded-lg border border-slate-200 bg-slate-50 p-3"> block.elements); + + const questions = getQuestionsFromBlocks(survey.blocks); const isDeleteDisabled = cardType === "question" ? questions.length === 1 : survey.type === "link" && survey.endings.length === 1; diff --git a/apps/web/modules/survey/editor/components/end-screen-form.tsx b/apps/web/modules/survey/editor/components/end-screen-form.tsx index 988bd7d2a1..cad4c7204d 100644 --- a/apps/web/modules/survey/editor/components/end-screen-form.tsx +++ b/apps/web/modules/survey/editor/components/end-screen-form.tsx @@ -9,6 +9,7 @@ import { createI18nString, extractLanguageCodes, getLocalizedValue } from "@/lib 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 { getQuestionsFromBlocks } from "@/modules/survey/editor/lib/blocks"; import { Button } from "@/modules/ui/components/button"; import { Input } from "@/modules/ui/components/input"; import { Label } from "@/modules/ui/components/label"; @@ -43,7 +44,7 @@ export const EndScreenForm = ({ const inputRef = useRef(null); const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages); - const questions = localSurvey.blocks.flatMap((block) => block.elements); + const questions = getQuestionsFromBlocks(localSurvey.blocks); const [showEndingCardCTA, setshowEndingCardCTA] = useState( endingCard.type === "endScreen" && diff --git a/apps/web/modules/survey/editor/components/hidden-fields-card.tsx b/apps/web/modules/survey/editor/components/hidden-fields-card.tsx index dba7c0f499..f21157d168 100644 --- a/apps/web/modules/survey/editor/components/hidden-fields-card.tsx +++ b/apps/web/modules/survey/editor/components/hidden-fields-card.tsx @@ -3,7 +3,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import * as Collapsible from "@radix-ui/react-collapsible"; import { EyeOff } from "lucide-react"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { TSurveyQuota } from "@formbricks/types/quota"; @@ -11,6 +11,7 @@ import { TSurvey, TSurveyHiddenFields, TSurveyQuestionId } from "@formbricks/typ import { validateId } from "@formbricks/types/surveys/validation"; import { cn } from "@/lib/cn"; import { extractRecallInfo } from "@/lib/utils/recall"; +import { getQuestionsFromBlocks } from "@/modules/survey/editor/lib/blocks"; import { findHiddenFieldUsedInLogic, isUsedInQuota, isUsedInRecall } from "@/modules/survey/editor/lib/utils"; import { Button } from "@/modules/ui/components/button"; import { Input } from "@/modules/ui/components/input"; @@ -44,6 +45,8 @@ export const HiddenFieldsCard = ({ } }; + const questions = useMemo(() => getQuestionsFromBlocks(localSurvey.blocks), [localSurvey.blocks]); + const updateSurvey = (data: TSurveyHiddenFields, currentFieldId?: string) => { let updatedSurvey = { ...localSurvey }; @@ -97,7 +100,8 @@ export const HiddenFieldsCard = ({ ); return; } - const totalQuestions = localSurvey.blocks.flatMap((b) => b.elements).length; + + const totalQuestions = questions.length; if (recallQuestionIdx === totalQuestions) { toast.error( t("environments.surveys.edit.hidden_field_used_in_recall_ending_card", { hiddenField: fieldId }) @@ -196,8 +200,7 @@ export const HiddenFieldsCard = ({ className="mt-5" onSubmit={(e) => { e.preventDefault(); - const existingElements = localSurvey.blocks.flatMap((b) => b.elements); - const existingQuestionIds = existingElements.map((question) => question.id); + const existingQuestionIds = questions.map((question) => question.id); const existingEndingCardIds = localSurvey.endings.map((ending) => ending.id); const existingHiddenFieldIds = localSurvey.hiddenFields.fieldIds ?? []; const validateIdError = validateId( diff --git a/apps/web/modules/survey/editor/components/logic-editor.tsx b/apps/web/modules/survey/editor/components/logic-editor.tsx index bd4bfb306e..34db08622f 100644 --- a/apps/web/modules/survey/editor/components/logic-editor.tsx +++ b/apps/web/modules/survey/editor/components/logic-editor.tsx @@ -10,6 +10,7 @@ import { getTextContent } from "@formbricks/types/surveys/validation"; import { recallToHeadline } from "@/lib/utils/recall"; import { LogicEditorActions } from "@/modules/survey/editor/components/logic-editor-actions"; import { LogicEditorConditions } from "@/modules/survey/editor/components/logic-editor-conditions"; +import { getQuestionsFromBlocks } from "@/modules/survey/editor/lib/blocks"; import { getQuestionIconMap } from "@/modules/survey/lib/questions"; import { Select, @@ -60,7 +61,7 @@ export function LogicEditor({ }[] = []; // Derive questions from blocks - const allQuestions = localSurvey.blocks.flatMap((b) => b.elements); + const allQuestions = getQuestionsFromBlocks(localSurvey.blocks); const blocks = localSurvey.blocks; // Track which blocks we've already added to avoid duplicates when a block has multiple elements diff --git a/apps/web/modules/survey/editor/components/questions-droppable.tsx b/apps/web/modules/survey/editor/components/questions-droppable.tsx index 3ccb70a499..dd597e9cec 100644 --- a/apps/web/modules/survey/editor/components/questions-droppable.tsx +++ b/apps/web/modules/survey/editor/components/questions-droppable.tsx @@ -7,6 +7,7 @@ import { TSurveyBlockLogic } from "@formbricks/types/surveys/blocks"; import { TSurvey, TSurveyQuestion, TSurveyQuestionId } from "@formbricks/types/surveys/types"; import { TUserLocale } from "@formbricks/types/user"; import { QuestionCard } from "@/modules/survey/editor/components/question-card"; +import { getQuestionsFromBlocks } from "@/modules/survey/editor/lib/blocks"; interface QuestionsDraggableProps { localSurvey: TSurvey; @@ -64,9 +65,7 @@ export const QuestionsDroppable = ({ const [parent] = useAutoAnimate(); // Derive questions from blocks for display - const questions = useMemo(() => { - return localSurvey.blocks.flatMap((block) => block.elements); - }, [localSurvey.blocks]); + const questions = useMemo(() => getQuestionsFromBlocks(localSurvey.blocks), [localSurvey.blocks]); return (
diff --git a/apps/web/modules/survey/editor/components/questions-view.tsx b/apps/web/modules/survey/editor/components/questions-view.tsx index 30c257dfae..365ba378bf 100644 --- a/apps/web/modules/survey/editor/components/questions-view.tsx +++ b/apps/web/modules/survey/editor/components/questions-view.tsx @@ -39,6 +39,8 @@ import { addBlock, deleteBlock, duplicateBlock, + findElementLocation, + getQuestionsFromBlocks, moveBlock, updateElementInBlock, } from "@/modules/survey/editor/lib/blocks"; @@ -96,9 +98,7 @@ export const QuestionsView = ({ const { t } = useTranslation(); // Derive questions from blocks for display - const questions = useMemo(() => { - return localSurvey.blocks.flatMap((block) => block.elements); - }, [localSurvey.blocks]); + const questions = useMemo(() => getQuestionsFromBlocks(localSurvey.blocks), [localSurvey.blocks]); const internalQuestionIdMap = useMemo(() => { return questions.reduce((acc, question) => { @@ -109,20 +109,6 @@ export const QuestionsView = ({ const surveyLanguages = localSurvey.languages; - const findElementLocation = (elementId: string) => { - const blocks = localSurvey.blocks; - - for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) { - const block = blocks[blockIndex]; - const elementIndex = block.elements.findIndex((e) => e.id === elementId); - if (elementIndex !== -1) { - return { blockId: block.id, blockIndex, elementIndex }; - } - } - - return { blockId: null, blockIndex: -1, elementIndex: -1 }; - }; - const getQuestionIdFromBlockId = (block: TSurveyBlock): string => block.elements[0].id; const getBlockName = (index: number): string => { @@ -279,7 +265,7 @@ export const QuestionsView = ({ const question = questions[questionIdx]; if (!question) return; - const { blockId, blockIndex } = findElementLocation(question.id); + const { blockId, blockIndex } = findElementLocation(localSurvey, question.id); if (!blockId || blockIndex === -1) return; let updatedSurvey = { ...localSurvey }; @@ -368,7 +354,7 @@ export const QuestionsView = ({ const question = questions[questionIdx]; if (!question) return; - const { blockIndex } = findElementLocation(question.id); + const { blockIndex } = findElementLocation(localSurvey, question.id); if (blockIndex === -1) return; setLocalSurvey((prevSurvey) => { @@ -386,7 +372,7 @@ export const QuestionsView = ({ const question = questions[questionIdx]; if (!question) return; - const { blockIndex } = findElementLocation(question.id); + const { blockIndex } = findElementLocation(localSurvey, question.id); if (blockIndex === -1) return; setLocalSurvey((prevSurvey) => { @@ -482,7 +468,7 @@ export const QuestionsView = ({ })); // Find and delete the block containing this question - const { blockId } = findElementLocation(questionId); + const { blockId } = findElementLocation(localSurvey, questionId); if (!blockId) return; const result = deleteBlock(updatedSurvey, blockId); @@ -511,7 +497,7 @@ export const QuestionsView = ({ const question = questions[questionIdx]; if (!question) return; - const { blockId } = findElementLocation(question.id); + const { blockId } = findElementLocation(localSurvey, question.id); if (!blockId) return; const result = duplicateBlock(localSurvey, blockId); @@ -523,7 +509,7 @@ export const QuestionsView = ({ // The duplicated block has new element IDs, find the first one const allBlocks = result.data.blocks ?? []; - const { blockIndex } = findElementLocation(question.id); + const { blockIndex } = findElementLocation(localSurvey, question.id); const duplicatedBlock = allBlocks[blockIndex + 1]; const newElementId = duplicatedBlock?.elements[0]?.id; @@ -572,7 +558,7 @@ export const QuestionsView = ({ const question = questions[questionIndex]; if (!question) return; - const { blockId } = findElementLocation(question.id); + const { blockId } = findElementLocation(localSurvey, question.id); if (!blockId) return; const direction = up ? "up" : "down"; @@ -633,8 +619,8 @@ export const QuestionsView = ({ if (!sourceQuestion || !destQuestion) return; - const { blockIndex: sourceBlockIndex } = findElementLocation(sourceQuestion.id); - const { blockIndex: destBlockIndex } = findElementLocation(destQuestion.id); + const { blockIndex: sourceBlockIndex } = findElementLocation(localSurvey, sourceQuestion.id); + const { blockIndex: destBlockIndex } = findElementLocation(localSurvey, destQuestion.id); if (sourceBlockIndex === -1 || destBlockIndex === -1) return; if (sourceBlockIndex === destBlockIndex) return; // No move needed diff --git a/apps/web/modules/survey/editor/components/survey-variables-card-item.tsx b/apps/web/modules/survey/editor/components/survey-variables-card-item.tsx index a51946982f..72e023c288 100644 --- a/apps/web/modules/survey/editor/components/survey-variables-card-item.tsx +++ b/apps/web/modules/survey/editor/components/survey-variables-card-item.tsx @@ -9,6 +9,7 @@ import { useTranslation } from "react-i18next"; import { TSurveyQuota } from "@formbricks/types/quota"; import { TSurvey, TSurveyVariable } from "@formbricks/types/surveys/types"; import { extractRecallInfo } from "@/lib/utils/recall"; +import { getQuestionsFromBlocks } from "@/modules/survey/editor/lib/blocks"; import { findVariableUsedInLogic, isUsedInQuota, isUsedInRecall } from "@/modules/survey/editor/lib/utils"; import { Button } from "@/modules/ui/components/button"; import { FormControl, FormField, FormItem, FormProvider } from "@/modules/ui/components/form"; @@ -78,7 +79,7 @@ export const SurveyVariablesCardItem = ({ // Removed auto-submit effect const onVariableDelete = (variableToDelete: TSurveyVariable) => { - const questions = localSurvey.blocks.flatMap((block) => block.elements); + const questions = getQuestionsFromBlocks(localSurvey.blocks); const quesIdx = findVariableUsedInLogic(localSurvey, variableToDelete.id); if (quesIdx !== -1) { diff --git a/apps/web/modules/survey/editor/components/update-question-id.tsx b/apps/web/modules/survey/editor/components/update-question-id.tsx index 1dc035fcdc..54d7cebbb1 100644 --- a/apps/web/modules/survey/editor/components/update-question-id.tsx +++ b/apps/web/modules/survey/editor/components/update-question-id.tsx @@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next"; import { TSurveyElement } from "@formbricks/types/surveys/elements"; import { TSurvey } from "@formbricks/types/surveys/types"; import { validateId } from "@formbricks/types/surveys/validation"; +import { getQuestionsFromBlocks } from "@/modules/survey/editor/lib/blocks"; import { Button } from "@/modules/ui/components/button"; import { Input } from "@/modules/ui/components/input"; import { Label } from "@/modules/ui/components/label"; @@ -36,7 +37,7 @@ export const UpdateQuestionId = ({ return; } - const questions = localSurvey.blocks.flatMap((block) => block.elements); + const questions = getQuestionsFromBlocks(localSurvey.blocks); const questionIds = questions.map((q) => q.id); const endingCardIds = localSurvey.endings.map((e) => e.id); const hiddenFieldIds = localSurvey.hiddenFields.fieldIds ?? []; diff --git a/apps/web/modules/survey/editor/lib/blocks.ts b/apps/web/modules/survey/editor/lib/blocks.ts index cd9583a4a2..0c4049c2fa 100644 --- a/apps/web/modules/survey/editor/lib/blocks.ts +++ b/apps/web/modules/survey/editor/lib/blocks.ts @@ -24,6 +24,32 @@ export const isElementIdUnique = (elementId: string, blocks: TSurveyBlock[]): bo return true; }; +/** + * Find the location of an element within the survey blocks + * @param survey - The survey object + * @param elementId - The ID of the element to find + * @returns Object containing blockId, blockIndex, elementIndex and the block + */ +export const findElementLocation = ( + survey: TSurvey, + elementId: string +): { blockId: string | null; blockIndex: number; elementIndex: number; block: TSurveyBlock | null } => { + const blocks = survey.blocks; + + for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) { + const block = blocks[blockIndex]; + const elementIndex = block.elements.findIndex((e) => e.id === elementId); + if (elementIndex !== -1) { + return { blockId: block.id, blockIndex, elementIndex, block }; + } + } + + return { blockId: null, blockIndex: -1, elementIndex: -1, block: null }; +}; + +export const getQuestionsFromBlocks = (blocks: TSurveyBlock[]): TSurveyElement[] => + blocks.flatMap((block) => block.elements); + // ============================================ // BLOCK OPERATIONS // ============================================ @@ -388,26 +414,3 @@ export const duplicateElementInBlock = ( blocks, }); }; - -/** - * Find the location of an element within the survey blocks - * @param survey - The survey object - * @param elementId - The ID of the element to find - * @returns Object containing blockId, blockIndex, and elementIndex - */ -export const findElementLocation = ( - survey: TSurvey, - elementId: string -): { blockId: string | null; blockIndex: number; elementIndex: number } => { - const blocks = survey.blocks; - - for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) { - const block = blocks[blockIndex]; - const elementIndex = block.elements.findIndex((e) => e.id === elementId); - if (elementIndex !== -1) { - return { blockId: block.id, blockIndex, elementIndex }; - } - } - - return { blockId: null, blockIndex: -1, elementIndex: -1 }; -}; diff --git a/apps/web/modules/survey/editor/lib/shared-conditions-factory.ts b/apps/web/modules/survey/editor/lib/shared-conditions-factory.ts index bada6afd40..bfd667d896 100644 --- a/apps/web/modules/survey/editor/lib/shared-conditions-factory.ts +++ b/apps/web/modules/survey/editor/lib/shared-conditions-factory.ts @@ -16,6 +16,7 @@ import { toggleGroupConnector, updateCondition, } from "@/lib/surveyLogic/utils"; +import { getQuestionsFromBlocks } from "@/modules/survey/editor/lib/blocks"; import { getConditionOperatorOptions, getConditionValueOptions, @@ -56,7 +57,7 @@ export function createSharedConditionsFactory( const { onConditionsChange } = updateCallbacks; // Derive questions from blocks - const questions = survey.blocks.flatMap((block) => block.elements.map((element) => element)); + const questions = getQuestionsFromBlocks(survey.blocks); // Handles special update logic for matrix questions, setting appropriate operators and metadata const handleMatrixQuestionUpdate = (resourceId: string, updates: Partial): boolean => { diff --git a/apps/web/modules/survey/editor/lib/utils.tsx b/apps/web/modules/survey/editor/lib/utils.tsx index 02e2822205..b9a51bb4d8 100644 --- a/apps/web/modules/survey/editor/lib/utils.tsx +++ b/apps/web/modules/survey/editor/lib/utils.tsx @@ -24,6 +24,7 @@ import { getTextContent } from "@formbricks/types/surveys/validation"; import { getLocalizedValue } from "@/lib/i18n/utils"; import { isConditionGroup } from "@/lib/surveyLogic/utils"; import { recallToHeadline } from "@/lib/utils/recall"; +import { findElementLocation, getQuestionsFromBlocks } from "@/modules/survey/editor/lib/blocks"; import { getQuestionTypes } from "@/modules/survey/lib/questions"; import { TComboboxGroupedOption, TComboboxOption } from "@/modules/ui/components/input-combo-box"; import { TLogicRuleOption, getLogicRules } from "./logic-rule-engine"; @@ -112,7 +113,7 @@ export const getConditionValueOptions = ( const hiddenFields = localSurvey.hiddenFields?.fieldIds ?? []; const variables = localSurvey.variables ?? []; // Derive questions from blocks - const questions = localSurvey.blocks.flatMap((block) => block.elements.map((element) => element)); + const questions = getQuestionsFromBlocks(localSurvey.blocks); const groupedOptions: TComboboxGroupedOption[] = []; const questionOptions: TComboboxOption[] = []; @@ -278,7 +279,7 @@ export const getDefaultOperatorForQuestion = ( export const getFormatLeftOperandValue = (condition: TSingleCondition, localSurvey: TSurvey): string => { if (condition.leftOperand.type === "question") { - const questions = localSurvey.blocks.flatMap((block) => block.elements.map((element) => element)); + const questions = getQuestionsFromBlocks(localSurvey.blocks); const question = questions.find((q) => q.id === condition.leftOperand.value); if (question && question.type === TSurveyElementTypeEnum.Matrix) { if (condition.leftOperand?.meta?.row !== undefined) { @@ -303,7 +304,7 @@ export const getConditionOperatorOptions = ( return getLogicRules(t).hiddenField.options; } else if (condition.leftOperand.type === "question") { // Derive questions from blocks - const questions = localSurvey.blocks.flatMap((block) => block.elements.map((element) => element)); + const questions = getQuestionsFromBlocks(localSurvey.blocks); const question = questions.find((question) => { let leftOperandQuestionId = condition.leftOperand.value; if (question.type === TSurveyElementTypeEnum.Matrix) { @@ -349,7 +350,7 @@ export const getMatchValueProps = ( } // Derive questions from blocks - const allQuestions = localSurvey.blocks.flatMap((block) => block.elements.map((element) => element)); + const allQuestions = getQuestionsFromBlocks(localSurvey.blocks); let questions = allQuestions.filter((_, idx) => typeof questionIdx === "undefined" ? true : idx <= questionIdx ); @@ -1068,7 +1069,7 @@ export const getActionValueOptions = ( questionIdx: number, t: TFunction ): TComboboxGroupedOption[] => { - const questions = localSurvey.blocks.flatMap((block) => block.elements.map((element) => element)); + const questions = getQuestionsFromBlocks(localSurvey.blocks); const hiddenFields = localSurvey.hiddenFields?.fieldIds ?? []; let variables = localSurvey.variables ?? []; const filteredQuestions = questions.filter((_, idx) => idx <= questionIdx); @@ -1250,6 +1251,13 @@ const isUsedInRightOperand = ( }; export const findQuestionUsedInLogic = (survey: TSurvey, questionId: TSurveyQuestionId): number => { + const { block } = findElementLocation(survey, questionId); + + // 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 + if (!block) { + return -1; + } + const isUsedInCondition = (condition: TSingleCondition | TConditionGroup): boolean => { if (isConditionGroup(condition)) { // It's a TConditionGroup @@ -1264,10 +1272,11 @@ export const findQuestionUsedInLogic = (survey: TSurvey, questionId: TSurveyQues }; const isUsedInAction = (action: TSurveyBlockLogicAction): boolean => { - return ( - // Note: jumpToBlock targets block IDs, not question IDs, so we only check requireAnswer - action.objective === "requireAnswer" && action.target === questionId - ); + if (action.objective === "requireAnswer" && action.target === questionId) { + return true; + } + + return action.objective === "jumpToBlock" && action.target === block.id; }; const isUsedInLogicRule = (logicRule: TSurveyBlockLogic): boolean => { @@ -1275,14 +1284,20 @@ export const findQuestionUsedInLogic = (survey: TSurvey, questionId: TSurveyQues }; // Derive questions from blocks (cast as questions to access logic properties) - const questions = (survey.blocks?.flatMap((b) => b.elements) ?? []) as unknown as TSurveyQuestion[]; + const questions = getQuestionsFromBlocks(survey.blocks); - return questions.findIndex( - (question) => - question.logicFallback === questionId || - (question.id !== questionId && - (question.logic as unknown as TSurveyBlockLogic[])?.some(isUsedInLogicRule)) - ); + return questions.findIndex((question) => { + const { block } = findElementLocation(survey, question.id); + + if (!block) { + return false; + } + + return ( + block.logicFallback === questionId || + (question.id !== questionId && block.logic?.some(isUsedInLogicRule)) + ); + }); }; export const isUsedInQuota = ( @@ -1439,11 +1454,17 @@ export const findOptionUsedInLogic = ( }; // Derive questions from blocks (cast as questions to access logic properties) - const questions = (survey.blocks?.flatMap((b) => b.elements) ?? []) as unknown as TSurveyQuestion[]; + const questions = getQuestionsFromBlocks(survey.blocks); - return questions.findIndex((question) => - (question.logic as unknown as TSurveyBlockLogic[])?.some(isUsedInLogicRule) - ); + return questions.findIndex((question) => { + const { block } = findElementLocation(survey, question.id); + + if (!block) { + return false; + } + + return block.logic?.some(isUsedInLogicRule); + }); }; export const findVariableUsedInLogic = (survey: TSurvey, variableId: string): number => { @@ -1471,9 +1492,15 @@ export const findVariableUsedInLogic = (survey: TSurvey, variableId: string): nu // Derive questions from blocks (cast as questions to access logic properties) const questions = (survey.blocks?.flatMap((b) => b.elements) ?? []) as unknown as TSurveyQuestion[]; - return questions.findIndex((question) => - (question.logic as unknown as TSurveyBlockLogic[])?.some(isUsedInLogicRule) - ); + return questions.findIndex((question) => { + const { block } = findElementLocation(survey, question.id); + + if (!block) { + return false; + } + + return block.logic?.some(isUsedInLogicRule); + }); }; export const findHiddenFieldUsedInLogic = (survey: TSurvey, hiddenFieldId: string): number => { @@ -1496,11 +1523,17 @@ export const findHiddenFieldUsedInLogic = (survey: TSurvey, hiddenFieldId: strin }; // Derive questions from blocks (cast as questions to access logic properties) - const questions = (survey.blocks?.flatMap((b) => b.elements) ?? []) as unknown as TSurveyQuestion[]; + const questions = getQuestionsFromBlocks(survey.blocks); - return questions.findIndex((question) => - (question.logic as unknown as TSurveyBlockLogic[])?.some(isUsedInLogicRule) - ); + return questions.findIndex((question) => { + const { block } = findElementLocation(survey, question.id); + + if (!block) { + return false; + } + + return block.logic?.some(isUsedInLogicRule); + }); }; export const getSurveyFollowUpActionDefaultBody = (t: TFunction): string => { @@ -1520,11 +1553,15 @@ export const findEndingCardUsedInLogic = (survey: TSurvey, endingCardId: string) }; // Derive questions from blocks (cast as questions to access logic properties) - const questions = (survey.blocks?.flatMap((b) => b.elements) ?? []) as unknown as TSurveyQuestion[]; + const questions = getQuestionsFromBlocks(survey.blocks); - return questions.findIndex( - (question) => - question.logicFallback === endingCardId || - (question.logic as unknown as TSurveyBlockLogic[])?.some(isUsedInLogicRule) - ); + return questions.findIndex((question) => { + const { block } = findElementLocation(survey, question.id); + + if (!block) { + return false; + } + + return block.logicFallback === endingCardId || block.logic?.some(isUsedInLogicRule); + }); }; diff --git a/apps/web/modules/survey/follow-ups/components/follow-up-modal.tsx b/apps/web/modules/survey/follow-ups/components/follow-up-modal.tsx index 59c6eea4e9..0eddb2da07 100644 --- a/apps/web/modules/survey/follow-ups/components/follow-up-modal.tsx +++ b/apps/web/modules/survey/follow-ups/components/follow-up-modal.tsx @@ -22,6 +22,7 @@ import { TSurvey } from "@formbricks/types/surveys/types"; import { TUserLocale } from "@formbricks/types/user"; import { getLocalizedValue } from "@/lib/i18n/utils"; import { recallToHeadline } from "@/lib/utils/recall"; +import { getQuestionsFromBlocks } from "@/modules/survey/editor/lib/blocks"; import { getSurveyFollowUpActionDefaultBody } from "@/modules/survey/editor/lib/utils"; import { TCreateSurveyFollowUpForm, @@ -102,7 +103,7 @@ export const FollowUpModal = ({ const emailSendToOptions: EmailSendToOption[] = useMemo(() => { // Derive questions from blocks - const questions = localSurvey.blocks.flatMap((block) => block.elements); + const questions = getQuestionsFromBlocks(localSurvey.blocks); const openTextAndContactQuestions = questions.filter((question) => { if (question.type === TSurveyElementTypeEnum.ContactInfo) {