fix: fixes sonar issues and coderabbit feedback (#6902)

This commit is contained in:
Anshuman Pandey
2025-11-28 15:43:11 +05:30
committed by GitHub
parent f141c53c68
commit 4e3dcae02d
13 changed files with 372 additions and 236 deletions

View File

@@ -1,7 +1,11 @@
import { createId } from "@paralleldrive/cuid2";
import { Result, err, ok } from "@formbricks/types/error-handlers";
import { InvalidInputError } from "@formbricks/types/errors";
import { type TSurveyBlock, TSurveyBlockLogicAction } from "@formbricks/types/surveys/blocks";
import {
type TSurveyBlock,
type TSurveyBlockLogic,
type TSurveyBlockLogicAction,
} from "@formbricks/types/surveys/blocks";
import { type TConditionGroup, type TSingleCondition } from "@formbricks/types/surveys/logic";
import {
type TSurveyEnding,
@@ -415,6 +419,26 @@ export const transformQuestionsToBlocks = (
return blocks as TSurveyBlock[];
};
const transformBlockLogicToQuestionLogic = (
blockLogic: TSurveyBlockLogic[],
blockIdToQuestionId: Map<string, string>,
endingIds: Set<string>
): unknown[] => {
return blockLogic.map((item) => {
const updatedConditions = convertElementToQuestionType(item.conditions);
if (!updatedConditions || !isConditionGroup(updatedConditions)) {
return item;
}
return {
...item,
conditions: updatedConditions,
actions: reverseLogicActions(item.actions, blockIdToQuestionId, endingIds),
};
});
};
export const transformBlocksToQuestions = (
blocks: TSurveyBlock[],
endings: TSurveyEnding[] = []
@@ -442,21 +466,7 @@ export const transformBlocksToQuestions = (
}
if (Array.isArray(block.logic) && block.logic.length > 0) {
element.logic = block.logic.map(
(item: { id: string; conditions: TConditionGroup; actions: TSurveyBlockLogicAction[] }) => {
const updatedConditions = convertElementToQuestionType(item.conditions);
if (!updatedConditions || !isConditionGroup(updatedConditions)) {
return item;
}
return {
...item,
conditions: updatedConditions,
actions: reverseLogicActions(item.actions, blockIdToQuestionId, endingIds),
};
}
);
element.logic = transformBlockLogicToQuestionLogic(block.logic, blockIdToQuestionId, endingIds);
}
if (block.logicFallback) {

View File

@@ -209,6 +209,12 @@ export const writeData = async (
responses: string[],
elements: string[]
) => {
if (responses.length !== elements.length) {
throw new Error(
`Array length mismatch: responses (${responses.length}) and elements (${elements.length}) must be equal`
);
}
// 1) Build the record payload
const data: Record<string, string> = {};
for (let i = 0; i < elements.length; i++) {

View File

@@ -30,7 +30,7 @@ export const createI18nString = (
return i18nString;
} else {
// It's a regular string, so create a new i18n object
const i18nString: any = {
const i18nString = {
[targetLanguageCode ?? "default"]: text,
};
@@ -46,7 +46,7 @@ export const createI18nString = (
};
// Type guard to check if an object is an I18nString
export const isI18nObject = (obj: any): obj is TI18nString => {
export const isI18nObject = (obj: unknown): obj is TI18nString => {
return typeof obj === "object" && obj !== null && Object.keys(obj).includes("default");
};
@@ -92,7 +92,7 @@ export const iso639Identifiers = iso639Languages.map((language) => language.alph
// Helper function to add language keys to a multi-language object (e.g. survey or question)
// Iterates over the object recursively and adds empty strings for new language keys
export const addMultiLanguageLabels = (object: any, languageSymbols: string[]): any => {
export const addMultiLanguageLabels = (object: unknown, languageSymbols: string[]): any => {
// Helper function to add language keys to a multi-language object
function addLanguageKeys(obj: { default: string; [key: string]: string }) {
languageSymbols.forEach((lang) => {
@@ -103,14 +103,14 @@ export const addMultiLanguageLabels = (object: any, languageSymbols: string[]):
}
// Recursive function to process an object or array
function processObject(obj: any) {
function processObject(obj: unknown) {
if (Array.isArray(obj)) {
obj.forEach((item) => processObject(item));
} else if (obj && typeof obj === "object") {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (key === "default" && typeof obj[key] === "string") {
addLanguageKeys(obj);
addLanguageKeys(obj as { default: string; [key: string]: string });
} else {
processObject(obj[key]);
}

View File

@@ -174,7 +174,7 @@ describe("Response Processing", () => {
});
});
describe("getQuestionResponseMapping", () => {
describe("getElementResponseMapping", () => {
const mockSurvey = {
id: "survey1",
type: "link" as const,

View File

@@ -351,7 +351,6 @@ describe("checkForInvalidMediaInBlocks", () => {
expect(result.ok).toBe(false);
if (!result.ok) {
console.log(result.error);
expect(result.error.message).toBe(
'Invalid image URL in choice 1 of question 1 of block "Welcome Block"'
);

View File

@@ -151,6 +151,8 @@ export const RecallItemSelect = ({
case "variable":
const variable = localSurvey.variables.find((variable) => variable.id === recallItem.id);
return variable?.type === "number" ? FileDigitIcon : FileTextIcon;
default:
return null;
}
};

View File

@@ -113,7 +113,7 @@ export const ElementFormInput = ({
: isEndingCard
? localSurvey.endings[elementIdx - elements.length].id
: currentElement.id;
//eslint-disable-next-line
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isWelcomeCard, isEndingCard, currentElement?.id]);
const endingCard = localSurvey.endings.find((ending) => ending.id === elementId);

View File

@@ -8,7 +8,6 @@ import toast from "react-hot-toast";
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 { findVariableUsedInLogic, isUsedInQuota, isUsedInRecall } from "@/modules/survey/editor/lib/utils";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { Button } from "@/modules/ui/components/button";
@@ -133,26 +132,9 @@ export const SurveyVariablesCardItem = ({
return;
}
// remove recall references from blocks
setLocalSurvey((prevSurvey) => {
const updatedBlocks = prevSurvey.blocks.map((block) => ({
...block,
elements: block.elements.map((element) => {
const updatedHeadline = { ...element.headline };
for (const [languageCode, headline] of Object.entries(element.headline)) {
if (headline.includes(`recall:${variableToDelete.id}`)) {
const recallInfo = extractRecallInfo(headline);
if (recallInfo) {
updatedHeadline[languageCode] = headline.replace(recallInfo, "");
}
}
}
return { ...element, headline: updatedHeadline };
}),
}));
const updatedVariables = prevSurvey.variables.filter((v) => v.id !== variableToDelete.id);
return { ...prevSurvey, variables: updatedVariables, blocks: updatedBlocks };
return { ...prevSurvey, variables: updatedVariables };
});
};

View File

@@ -1,5 +1,12 @@
import { TResponseData } from "@formbricks/types/responses";
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import {
TSurveyCTAElement,
TSurveyConsentElement,
TSurveyElement,
TSurveyElementTypeEnum,
TSurveyMultipleChoiceElement,
TSurveyRatingElement,
} from "@formbricks/types/surveys/elements";
import { TSurvey } from "@formbricks/types/surveys/types";
import { FORBIDDEN_IDS } from "@formbricks/types/surveys/validation";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
@@ -36,72 +43,135 @@ export const getPrefillValue = (
return Object.keys(prefillAnswer).length > 0 ? prefillAnswer : undefined;
};
const validateOpenText = (): boolean => {
return true;
};
const validateMultipleChoiceSingle = (
question: TSurveyMultipleChoiceElement,
answer: string,
language: string
): boolean => {
if (question.type !== TSurveyElementTypeEnum.MultipleChoiceSingle) return false;
const choices = question.choices;
const hasOther = choices[choices.length - 1].id === "other";
if (!hasOther) {
return choices.some((choice) => choice.label[language] === answer);
}
const matchesAnyChoice = choices.some((choice) => choice.label[language] === answer);
if (matchesAnyChoice) {
return true;
}
const trimmedAnswer = answer.trim();
return trimmedAnswer !== "";
};
const validateMultipleChoiceMulti = (question: TSurveyElement, answer: string, language: string): boolean => {
if (question.type !== TSurveyElementTypeEnum.MultipleChoiceMulti) return false;
const choices = (
question as TSurveyElement & { choices: Array<{ id: string; label: Record<string, string> }> }
).choices;
const hasOther = choices[choices.length - 1].id === "other";
const lastChoiceLabel = hasOther ? choices[choices.length - 1].label[language] : undefined;
const answerChoices = answer
.split(",")
.map((ans) => ans.trim())
.filter((ans) => ans !== "");
if (answerChoices.length === 0) {
return false;
}
if (!hasOther) {
return answerChoices.every((ans: string) => choices.some((choice) => choice.label[language] === ans));
}
let freeTextOtherCount = 0;
for (const ans of answerChoices) {
const matchesChoice = choices.some((choice) => choice.label[language] === ans);
if (matchesChoice) {
continue;
}
if (ans === lastChoiceLabel) {
continue;
}
freeTextOtherCount++;
if (freeTextOtherCount > 1) {
return false;
}
}
return true;
};
const validateNPS = (answer: string): boolean => {
try {
const cleanedAnswer = answer.replace(/&/g, ";");
const answerNumber = Number(JSON.parse(cleanedAnswer));
return !isNaN(answerNumber) && answerNumber >= 0 && answerNumber <= 10;
} catch {
return false;
}
};
const validateCTA = (question: TSurveyCTAElement, answer: string): boolean => {
if (question.required && answer === "dismissed") return false;
return answer === "clicked" || answer === "dismissed";
};
const validateConsent = (question: TSurveyConsentElement, answer: string): boolean => {
if (question.required && answer === "dismissed") return false;
return answer === "accepted" || answer === "dismissed";
};
const validateRating = (question: TSurveyRatingElement, answer: string): boolean => {
if (question.type !== TSurveyElementTypeEnum.Rating) return false;
const ratingQuestion = question;
try {
const cleanedAnswer = answer.replace(/&/g, ";");
const answerNumber = Number(JSON.parse(cleanedAnswer));
return answerNumber >= 1 && answerNumber <= (ratingQuestion.range ?? 5);
} catch {
return false;
}
};
const validatePictureSelection = (answer: string): boolean => {
const answerChoices = answer.split(",");
return answerChoices.every((ans: string) => !isNaN(Number(ans)));
};
const checkValidity = (question: TSurveyElement, answer: string, language: string): boolean => {
if (question.required && (!answer || answer === "")) return false;
const validators: Partial<
Record<TSurveyElementTypeEnum, (q: TSurveyElement, a: string, l: string) => boolean>
> = {
[TSurveyElementTypeEnum.OpenText]: () => validateOpenText(),
[TSurveyElementTypeEnum.MultipleChoiceSingle]: (q, a, l) =>
validateMultipleChoiceSingle(q as TSurveyMultipleChoiceElement, a, l),
[TSurveyElementTypeEnum.MultipleChoiceMulti]: (q, a, l) => validateMultipleChoiceMulti(q, a, l),
[TSurveyElementTypeEnum.NPS]: (_, a) => validateNPS(a),
[TSurveyElementTypeEnum.CTA]: (q, a) => validateCTA(q as TSurveyCTAElement, a),
[TSurveyElementTypeEnum.Consent]: (q, a) => validateConsent(q as TSurveyConsentElement, a),
[TSurveyElementTypeEnum.Rating]: (q, a) => validateRating(q as TSurveyRatingElement, a),
[TSurveyElementTypeEnum.PictureSelection]: (_, a) => validatePictureSelection(a),
};
const validator = validators[question.type];
if (!validator) return false;
try {
switch (question.type) {
case TSurveyElementTypeEnum.OpenText: {
return true;
}
case TSurveyElementTypeEnum.MultipleChoiceSingle: {
const hasOther = question.choices[question.choices.length - 1].id === "other";
if (!hasOther) {
if (!question.choices.find((choice) => choice.label[language] === answer)) return false;
return true;
}
if (question.choices[question.choices.length - 1].label[language] === answer) {
return false;
}
return true;
}
case TSurveyElementTypeEnum.MultipleChoiceMulti: {
const answerChoices = answer.split(",");
const hasOther = question.choices[question.choices.length - 1].id === "other";
if (!hasOther) {
if (
!answerChoices.every((ans: string) =>
question.choices.find((choice) => choice.label[language] === ans)
)
)
return false;
return true;
}
return true;
}
case TSurveyElementTypeEnum.NPS: {
const cleanedAnswer = answer.replace(/&/g, ";");
const answerNumber = Number(JSON.parse(cleanedAnswer));
if (isNaN(answerNumber)) return false;
if (answerNumber < 0 || answerNumber > 10) return false;
return true;
}
case TSurveyElementTypeEnum.CTA: {
if (question.required && answer === "dismissed") return false;
if (answer !== "clicked" && answer !== "dismissed") return false;
return true;
}
case TSurveyElementTypeEnum.Consent: {
if (question.required && answer === "dismissed") return false;
if (answer !== "accepted" && answer !== "dismissed") return false;
return true;
}
case TSurveyElementTypeEnum.Rating: {
const cleanedAnswer = answer.replace(/&/g, ";");
const answerNumber = Number(JSON.parse(cleanedAnswer));
if (answerNumber < 1 || answerNumber > question.range) return false;
return true;
}
case TSurveyElementTypeEnum.PictureSelection: {
const answerChoices = answer.split(",");
return answerChoices.every((ans: string) => !isNaN(Number(ans)));
}
default:
return false;
}
} catch (e) {
return validator(question, answer, language);
} catch {
return false;
}
};

View File

@@ -3,7 +3,11 @@ import { type TJsFileUploadParams } from "@formbricks/types/js";
import { type TResponseData, type TResponseTtc } from "@formbricks/types/responses";
import { type TUploadFileConfig } from "@formbricks/types/storage";
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import {
TSurveyElement,
TSurveyElementTypeEnum,
TSurveyRankingElement,
} from "@formbricks/types/surveys/elements";
import { BackButton } from "@/components/buttons/back-button";
import { SubmitButton } from "@/components/buttons/submit-button";
import { ElementConditional } from "@/components/general/element-conditional";
@@ -108,82 +112,85 @@ export function BlockConditional({
// eslint-disable-next-line react-hooks/exhaustive-deps -- Only run once when block mounts
}, []);
const handleBlockSubmit = (e?: Event) => {
if (e) {
e.preventDefault();
// Validate ranking element
const validateRankingElement = (
element: TSurveyRankingElement,
response: unknown,
form: HTMLFormElement
): boolean => {
const rankingElement = element;
const hasIncompleteRanking =
(rankingElement.required &&
(!Array.isArray(response) || response.length !== rankingElement.choices.length)) ||
(!rankingElement.required &&
Array.isArray(response) &&
response.length > 0 &&
response.length < rankingElement.choices.length);
if (hasIncompleteRanking) {
form.requestSubmit();
return false;
}
return true;
};
// Check if response is empty
const isEmptyResponse = (response: unknown): boolean => {
return (
response === undefined ||
response === null ||
response === "" ||
(Array.isArray(response) && response.length === 0) ||
(typeof response === "object" && !Array.isArray(response) && Object.keys(response).length === 0)
);
};
// Validate a single element's form
const validateElementForm = (element: TSurveyElement, form: HTMLFormElement): boolean => {
// Check HTML5 validity first
if (!form.checkValidity()) {
form.reportValidity();
return false;
}
// Validate all forms and check for custom validation rules
const response = value[element.id];
// Custom validation for ranking questions
if (element.type === TSurveyElementTypeEnum.Ranking && !validateRankingElement(element, response, form)) {
return false;
}
// For other element types, check if required fields are empty
if (element.required && isEmptyResponse(response)) {
form.requestSubmit();
return false;
}
return true;
};
// Find the first invalid form
const findFirstInvalidForm = (): HTMLFormElement | null => {
let firstInvalidForm: HTMLFormElement | null = null;
for (const element of block.elements) {
const form = elementFormRefs.current.get(element.id);
if (form) {
// Check HTML5 validity first
if (!form.checkValidity()) {
if (!firstInvalidForm) {
firstInvalidForm = form;
}
form.reportValidity();
continue;
}
// Custom validation for ranking questions
if (element.type === TSurveyElementTypeEnum.Ranking) {
const response = value[element.id];
const rankingElement = element;
// Check if ranking is incomplete
const hasIncompleteRanking =
(rankingElement.required &&
(!Array.isArray(response) || response.length !== rankingElement.choices.length)) ||
(!rankingElement.required &&
Array.isArray(response) &&
response.length > 0 &&
response.length < rankingElement.choices.length);
if (hasIncompleteRanking) {
// Trigger the ranking form's submit to show the error message
form.requestSubmit();
if (!firstInvalidForm) {
firstInvalidForm = form;
}
continue;
}
}
// For other element types, check if required fields are empty
if (element.required) {
const response = value[element.id];
const isEmpty =
response === undefined ||
response === null ||
response === "" ||
(Array.isArray(response) && response.length === 0) ||
(typeof response === "object" && !Array.isArray(response) && Object.keys(response).length === 0);
if (isEmpty) {
form.requestSubmit();
if (!firstInvalidForm) {
firstInvalidForm = form;
}
continue;
}
if (form && !validateElementForm(element, form)) {
if (!firstInvalidForm) {
firstInvalidForm = form;
}
}
}
// If any form is invalid, scroll to it and stop
if (firstInvalidForm) {
firstInvalidForm.scrollIntoView({ behavior: "smooth", block: "center" });
return;
}
return firstInvalidForm;
};
// Collect TTC values from forms
const collectTtcValues = (): TResponseTtc => {
// Clear the TTC collector before collecting new values
ttcCollectorRef.current = {};
// Call each form's submit method to trigger TTC calculation
// The forms will call handleTtcCollect synchronously with their TTC values
block.elements.forEach((element) => {
const form = elementFormRefs.current.get(element.id);
if (form) {
@@ -192,10 +199,8 @@ export function BlockConditional({
});
// Collect TTC from the ref (populated synchronously by form submissions)
// Falls back to state for elements that may have TTC from user interactions
const blockTtc: TResponseTtc = {};
block.elements.forEach((element) => {
// Prefer freshly calculated TTC from form submission, fall back to state
if (ttcCollectorRef.current[element.id] !== undefined) {
blockTtc[element.id] = ttcCollectorRef.current[element.id];
} else if (ttc[element.id] !== undefined) {
@@ -203,14 +208,37 @@ export function BlockConditional({
}
});
// Collect responses for all elements in this block
return blockTtc;
};
// Collect responses for all elements in this block
const collectBlockResponses = (): TResponseData => {
const blockResponses: TResponseData = {};
block.elements.forEach((element) => {
if (value[element.id] !== undefined) {
blockResponses[element.id] = value[element.id];
}
});
return blockResponses;
};
const handleBlockSubmit = (e?: Event) => {
if (e) {
e.preventDefault();
}
// Validate all forms and check for custom validation rules
const firstInvalidForm = findFirstInvalidForm();
// If any form is invalid, scroll to it and stop
if (firstInvalidForm) {
firstInvalidForm.scrollIntoView({ behavior: "smooth", block: "center" });
return;
}
// Collect TTC and responses, then submit
const blockTtc = collectTtcValues();
const blockResponses = collectBlockResponses();
onSubmit(blockResponses, blockTtc);
};

View File

@@ -123,6 +123,7 @@ export function ElementConditional({
return null;
}
// NOSONAR - This is readable enough and can't be changed
const renderElement = () => {
switch (element.type) {
case TSurveyElementTypeEnum.OpenText:

View File

@@ -9,6 +9,8 @@ import type {
TResponseVariables,
} from "@formbricks/types/responses";
import { TUploadFileConfig } from "@formbricks/types/storage";
import { TSurveyBlock, TSurveyBlockLogic } from "@formbricks/types/surveys/blocks";
import { TSurveyElement } from "@formbricks/types/surveys/elements";
import { BlockConditional } from "@/components/general/block-conditional";
import { EndingCard } from "@/components/general/ending-card";
import { ErrorComponent } from "@/components/general/error-component";
@@ -350,20 +352,21 @@ export function Survey({
};
const makeQuestionsRequired = (requiredQuestionIds: string[]): void => {
const updateElementIfRequired = (element: TSurveyElement) => {
if (requiredQuestionIds.includes(element.id)) {
return { ...element, required: true };
}
return element;
};
const updateBlockElements = (block: TSurveyBlock) => ({
...block,
elements: block.elements.map(updateElementIfRequired),
});
setlocalSurvey((prevSurvey) => ({
...prevSurvey,
blocks: prevSurvey.blocks.map((block) => ({
...block,
elements: block.elements.map((element) => {
if (requiredQuestionIds.includes(element.id)) {
return {
...element,
required: true,
};
}
return element;
}),
})),
blocks: prevSurvey.blocks.map(updateBlockElements),
}));
};
@@ -371,20 +374,24 @@ export function Survey({
const questionsToRevert = questionRequiredByMap.current[questionId] || [];
if (questionsToRevert.length > 0) {
const revertElementIfNeeded = (element: TSurveyElement) => {
if (questionsToRevert.includes(element.id)) {
return {
...element,
required: originalQuestionRequiredStates[element.id] ?? element.required,
};
}
return element;
};
const updateBlockElements = (block: TSurveyBlock) => ({
...block,
elements: block.elements.map(revertElementIfNeeded),
});
setlocalSurvey((prevSurvey) => ({
...prevSurvey,
blocks: prevSurvey.blocks.map((block) => ({
...block,
elements: block.elements.map((element) => {
if (questionsToRevert.includes(element.id)) {
return {
...element,
required: originalQuestionRequiredStates[element.id] ?? element.required,
};
}
return element;
}),
})),
blocks: prevSurvey.blocks.map(updateBlockElements),
}));
// remove the question from the map
@@ -428,54 +435,85 @@ export function Survey({
throw new Error("Block not found");
}
let firstJumpTarget: string | undefined;
const allRequiredQuestionIds: string[] = [];
let calculationResults = { ...currentVariables };
const localResponseData = { ...responseData, ...data };
let calculationResults = { ...currentVariables };
// Process a single logic rule
const processLogicRule = (
logic: TSurveyBlockLogic,
currentJumpTarget: string | undefined,
currentRequiredIds: string[]
): { jumpTarget: string | undefined; requiredIds: string[]; updatedCalculations: TResponseVariables } => {
const isLogicMet = evaluateLogic(
localSurvey,
localResponseData,
calculationResults,
logic.conditions,
selectedLanguage
);
if (!isLogicMet) {
return {
jumpTarget: currentJumpTarget,
requiredIds: currentRequiredIds,
updatedCalculations: calculationResults,
};
}
const { jumpTarget, requiredQuestionIds, calculations } = performActions(
localSurvey,
logic.actions,
localResponseData,
calculationResults
);
const newJumpTarget = jumpTarget && !currentJumpTarget ? jumpTarget : currentJumpTarget;
const newRequiredIds = [...currentRequiredIds, ...requiredQuestionIds];
const updatedCalculations = { ...calculationResults, ...calculations };
return {
jumpTarget: newJumpTarget,
requiredIds: newRequiredIds,
updatedCalculations,
};
};
// Evaluate block-level logic
if (currentBlock.logic && currentBlock.logic.length > 0) {
for (const logic of currentBlock.logic) {
if (
evaluateLogic(
localSurvey,
localResponseData,
calculationResults,
logic.conditions,
selectedLanguage
)
) {
const { jumpTarget, requiredQuestionIds, calculations } = performActions(
localSurvey,
logic.actions,
localResponseData,
calculationResults
);
const evaluateBlockLogic = () => {
let firstJumpTarget: string | undefined;
const allRequiredQuestionIds: string[] = [];
if (jumpTarget && !firstJumpTarget) {
firstJumpTarget = jumpTarget; // This is already a block ID from performActions
}
allRequiredQuestionIds.push(...requiredQuestionIds);
calculationResults = { ...calculationResults, ...calculations };
if (currentBlock.logic && currentBlock.logic.length > 0) {
for (const logic of currentBlock.logic) {
const result = processLogicRule(logic, firstJumpTarget, allRequiredQuestionIds);
firstJumpTarget = result.jumpTarget;
allRequiredQuestionIds.length = 0;
allRequiredQuestionIds.push(...result.requiredIds);
calculationResults = result.updatedCalculations;
}
}
}
// Use logicFallback if no jump target was set (logicFallback is at block level)
if (!firstJumpTarget && currentBlock.logicFallback) {
firstJumpTarget = currentBlock.logicFallback;
}
if (allRequiredQuestionIds.length > 0) {
// Track which questions are being made required by this block
if (currentBlock.elements[0]) {
questionRequiredByMap.current[currentBlock.elements[0].id] = allRequiredQuestionIds;
// Use logicFallback if no jump target was set
if (!firstJumpTarget && currentBlock.logicFallback) {
firstJumpTarget = currentBlock.logicFallback;
}
makeQuestionsRequired(allRequiredQuestionIds);
}
return { firstJumpTarget, allRequiredQuestionIds };
};
const { firstJumpTarget, allRequiredQuestionIds } = evaluateBlockLogic();
// Handle required questions
const handleRequiredQuestions = (requiredIds: string[]) => {
if (requiredIds.length > 0) {
if (currentBlock.elements[0]) {
questionRequiredByMap.current[currentBlock.elements[0].id] = requiredIds;
}
makeQuestionsRequired(requiredIds);
}
};
handleRequiredQuestions(allRequiredQuestionIds);
// Return the jump target (which is a block ID) or the next block in sequence
const nextBlockId = firstJumpTarget || localSurvey.blocks[currentBlockIndex + 1]?.id;

View File

@@ -313,7 +313,7 @@ export const validateId = (
const combinedIds = [...existingElementIds, ...existingHiddenFieldIds, ...existingEndingCardIds];
if (combinedIds.findIndex((id) => id.toLowerCase() === field.toLowerCase()) !== -1) {
return `${type} ID already exists in questions, hidden fields, or elements.`;
return `${type} ID already exists in questions or hidden fields`;
}
if (FORBIDDEN_IDS.includes(field)) {