mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-04 18:49:39 -06:00
fix(validation): fix cyclic logic detection and add choice ID validation in block logic
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user