fix(validation): fix cyclic logic detection and add choice ID validation in block logic

This commit is contained in:
pandeymangg
2025-10-30 15:33:46 +05:30
parent 9790b071d7
commit ce4b64da0e
3 changed files with 108 additions and 14 deletions

View File

@@ -18,9 +18,11 @@ export const findBlocksWithCyclicLogic = (blocks: TSurveyBlock[]): TSurveyBlockI
const destination = jumpAction.target;
if (!visited[destination] && checkForCyclicLogic(destination)) {
cyclicBlocks.add(blockId);
recStack[blockId] = false;
return true;
} else if (recStack[destination]) {
cyclicBlocks.add(blockId);
recStack[blockId] = false;
return true;
}
}
@@ -32,9 +34,11 @@ export const findBlocksWithCyclicLogic = (blocks: TSurveyBlock[]): TSurveyBlockI
const fallbackBlockId = block.logicFallback;
if (!visited[fallbackBlockId] && checkForCyclicLogic(fallbackBlockId)) {
cyclicBlocks.add(blockId);
recStack[blockId] = false;
return true;
} else if (recStack[fallbackBlockId]) {
cyclicBlocks.add(blockId);
recStack[blockId] = false;
return true;
}
}
@@ -42,8 +46,16 @@ export const findBlocksWithCyclicLogic = (blocks: TSurveyBlock[]): TSurveyBlockI
// Handle default behavior: move to the next block if no jump actions or fallback logic is defined
const nextBlockIndex = blocks.findIndex((b) => b.id === blockId) + 1;
const nextBlock = blocks[nextBlockIndex] as TSurveyBlock | undefined;
if (nextBlock && !visited[nextBlock.id] && checkForCyclicLogic(nextBlock.id)) {
return true;
if (nextBlock) {
if (!visited[nextBlock.id] && checkForCyclicLogic(nextBlock.id)) {
cyclicBlocks.add(blockId);
recStack[blockId] = false;
return true;
} else if (recStack[nextBlock.id]) {
cyclicBlocks.add(blockId);
recStack[blockId] = false;
return true;
}
}
}

View File

@@ -103,7 +103,10 @@ export const findLanguageCodesForDuplicateLabels = (
const duplicateLabels = new Set<string>();
for (const language of languagesToCheck) {
const labelTexts = labels.map((label) => label[language].trim()).filter(Boolean);
const labelTexts = labels
.map((label) => label[language])
.filter((text): text is string => typeof text === "string" && text.trim().length > 0)
.map((text) => text.trim());
const uniqueLabels = new Set(labelTexts);
if (uniqueLabels.size !== labelTexts.length) {

View File

@@ -2818,11 +2818,28 @@ const isInvalidOperatorsForElementType = (
}
break;
case TSurveyElementTypeEnum.MultipleChoiceSingle:
case TSurveyElementTypeEnum.MultipleChoiceMulti:
if (!["equals", "doesNotEqual", "isSubmitted", "isSkipped"].includes(operator)) {
isInvalidOperator = true;
}
break;
case TSurveyElementTypeEnum.MultipleChoiceMulti:
case TSurveyElementTypeEnum.PictureSelection:
case TSurveyElementTypeEnum.Ranking:
if (
![
"equals",
"doesNotEqual",
"includesAllOf",
"includesOneOf",
"doesNotIncludeAllOf",
"doesNotIncludeOneOf",
"isSubmitted",
"isSkipped",
].includes(operator)
) {
isInvalidOperator = true;
}
break;
case TSurveyElementTypeEnum.NPS:
case TSurveyElementTypeEnum.Rating:
if (
@@ -2850,11 +2867,6 @@ const isInvalidOperatorsForElementType = (
isInvalidOperator = true;
}
break;
case TSurveyElementTypeEnum.PictureSelection:
if (!["equals", "doesNotEqual", "isSubmitted", "isSkipped"].includes(operator)) {
isInvalidOperator = true;
}
break;
case TSurveyElementTypeEnum.Cal:
if (!["isBooked", "isSkipped"].includes(operator)) {
isInvalidOperator = true;
@@ -2912,11 +2924,6 @@ const isInvalidOperatorsForElementType = (
isInvalidOperator = true;
}
break;
case TSurveyElementTypeEnum.Ranking:
if (!["equals", "doesNotEqual", "isSubmitted", "isSkipped"].includes(operator)) {
isInvalidOperator = true;
}
break;
}
return isInvalidOperator;
@@ -3075,6 +3082,78 @@ const validateBlockConditions = (
message: `Conditional Logic: Right operand should be a string for "${operator}" in logic no: ${String(logicIndex + 1)} of block ${String(blockIndex + 1)}`,
path: ["blocks", blockIndex, "logic", logicIndex, "conditions"],
});
} else {
// Validate that the choice ID exists in the element's choices
const choiceMatch = element.choices.find((c) => c.id === rightOperand.value);
if (!choiceMatch) {
issues.push({
code: z.ZodIssueCode.custom,
message: `Conditional Logic: Choice "${rightOperand.value}" does not exist in element ${String(elementInfo.element + 1)} of block ${String(elementInfo.block + 1)}`,
path: ["blocks", blockIndex, "logic", logicIndex, "conditions"],
});
}
}
}
} else if (
element.type === TSurveyElementTypeEnum.MultipleChoiceMulti ||
element.type === TSurveyElementTypeEnum.PictureSelection ||
element.type === TSurveyElementTypeEnum.Ranking
) {
if (rightOperand?.type !== "static") {
issues.push({
code: z.ZodIssueCode.custom,
message: `Conditional Logic: Right operand should be a static value for "${operator}" in logic no: ${String(logicIndex + 1)} of block ${String(blockIndex + 1)}`,
path: ["blocks", blockIndex, "logic", logicIndex, "conditions"],
});
} else if (condition.operator === "equals" || condition.operator === "doesNotEqual") {
if (typeof rightOperand.value !== "string") {
issues.push({
code: z.ZodIssueCode.custom,
message: `Conditional Logic: Right operand should be a string for "${operator}" in logic no: ${String(logicIndex + 1)} of block ${String(blockIndex + 1)}`,
path: ["blocks", blockIndex, "logic", logicIndex, "conditions"],
});
} else {
// Validate that the choice ID exists in the element's choices
const choiceMatch = element.choices.find((c) => c.id === rightOperand.value);
if (!choiceMatch) {
issues.push({
code: z.ZodIssueCode.custom,
message: `Conditional Logic: Choice "${rightOperand.value}" does not exist in element ${String(elementInfo.element + 1)} of block ${String(elementInfo.block + 1)}`,
path: ["blocks", blockIndex, "logic", logicIndex, "conditions"],
});
}
}
} else if (
["includesAllOf", "includesOneOf", "doesNotIncludeAllOf", "doesNotIncludeOneOf"].includes(
condition.operator
)
) {
if (!Array.isArray(rightOperand.value)) {
issues.push({
code: z.ZodIssueCode.custom,
message: `Conditional Logic: Right operand should be an array for "${operator}" in logic no: ${String(logicIndex + 1)} of block ${String(blockIndex + 1)}`,
path: ["blocks", blockIndex, "logic", logicIndex, "conditions"],
});
} else {
rightOperand.value.forEach((value) => {
if (typeof value !== "string") {
issues.push({
code: z.ZodIssueCode.custom,
message: `Conditional Logic: Each value in the right operand should be a string for "${operator}" in logic no: ${String(logicIndex + 1)} of block ${String(blockIndex + 1)}`,
path: ["blocks", blockIndex, "logic", logicIndex, "conditions"],
});
}
});
// Validate that all choice IDs exist in the element's choices
const choiceIds = element.choices.map((c) => c.id);
if (rightOperand.value.some((value) => !choiceIds.includes(value))) {
issues.push({
code: z.ZodIssueCode.custom,
message: `Conditional Logic: One or more choices selected in right operand do not exist in the element in logic no: ${String(logicIndex + 1)} of block ${String(blockIndex + 1)}`,
path: ["blocks", blockIndex, "logic", logicIndex, "conditions"],
});
}
}
}
}