Introduced validation logic options (and , or)

This commit is contained in:
Dhruwang
2026-01-09 12:25:50 +05:30
parent dd1c525ed7
commit e8345d3617
20 changed files with 143 additions and 16 deletions
+2
View File
@@ -1539,6 +1539,8 @@ checksums:
environments/surveys/edit/validation/pattern: c6f01d7bc9baa21a40ea38fa986bd5a0
environments/surveys/edit/validation/phone: bcd7bd37a475ab1f80ea4c5b4d4d0bb5
environments/surveys/edit/validation/url: 4006a4d8dfac013758f0053f6aa67cdd
environments/surveys/edit/validation_logic_and: 3d40ea5f8b18bc7e5129b20c7195f3e0
environments/surveys/edit/validation_logic_or: a2209364b5c65b4817add1b06ce11b02
environments/surveys/edit/validation_rules: 0cd99f02684d633196c8b249e857d207
environments/surveys/edit/validation_rules_description: a0a7cee05e18efd462148698e3a93399
environments/surveys/edit/variable_is_used_in_logic_of_question_please_remove_it_from_logic_first: bd9d9c7cf0be671c4e8cf67e2ae6659e
+2
View File
@@ -1614,6 +1614,8 @@
"phone": "Ist gültige Telefonnummer",
"url": "Ist gültige URL"
},
"validation_logic_and": "UND (alle müssen erfüllt sein)",
"validation_logic_or": "ODER (mindestens eine muss erfüllt sein)",
"validation_rules": "Validierungsregeln",
"validation_rules_description": "Nur Antworten akzeptieren, die die folgenden Kriterien erfüllen",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} wird in der Logik der Frage {questionIndex} verwendet. Bitte entferne es zuerst aus der Logik.",
+2
View File
@@ -1614,6 +1614,8 @@
"phone": "Is valid phone",
"url": "Is valid URL"
},
"validation_logic_and": "AND (all must pass)",
"validation_logic_or": "OR (at least one must pass)",
"validation_rules": "Validation rules",
"validation_rules_description": "Only accept responses that meet the following criteria",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} is used in logic of question {questionIndex}. Please remove it from logic first.",
+2
View File
@@ -1614,6 +1614,8 @@
"phone": "Es un teléfono válido",
"url": "Es una URL válida"
},
"validation_logic_and": "Y (todas deben cumplirse)",
"validation_logic_or": "O (al menos una debe cumplirse)",
"validation_rules": "Reglas de validación",
"validation_rules_description": "Solo aceptar respuestas que cumplan los siguientes criterios",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} se usa en la lógica de la pregunta {questionIndex}. Por favor, elimínala primero de la lógica.",
+2
View File
@@ -1614,6 +1614,8 @@
"phone": "Est un numéro de téléphone valide",
"url": "Est une URL valide"
},
"validation_logic_and": "ET (tous doivent réussir)",
"validation_logic_or": "OU (au moins un doit réussir)",
"validation_rules": "Règles de validation",
"validation_rules_description": "Accepter uniquement les réponses qui répondent aux critères suivants",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} est utilisé dans la logique de la question {questionIndex}. Veuillez d'abord le supprimer de la logique.",
+2
View File
@@ -1614,6 +1614,8 @@
"phone": "有効な電話番号である",
"url": "有効なURLである"
},
"validation_logic_and": "AND(すべて満たす必要があります)",
"validation_logic_or": "OR(少なくとも1つ満たす必要があります)",
"validation_rules": "検証ルール",
"validation_rules_description": "次の条件を満たす回答のみを受け付ける",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} は質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
+2
View File
@@ -1614,6 +1614,8 @@
"phone": "Is geldig telefoonnummer",
"url": "Is geldige URL"
},
"validation_logic_and": "EN (alle moeten slagen)",
"validation_logic_or": "OF (minimaal één moet slagen)",
"validation_rules": "Validatieregels",
"validation_rules_description": "Accepteer alleen antwoorden die voldoen aan de volgende criteria",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} wordt gebruikt in de logica van vraag {questionIndex}. Verwijder het eerst uit de logica.",
+2
View File
@@ -1614,6 +1614,8 @@
"phone": "É um telefone válido",
"url": "É uma URL válida"
},
"validation_logic_and": "E (todas devem passar)",
"validation_logic_or": "OU (pelo menos uma deve passar)",
"validation_rules": "Regras de validação",
"validation_rules_description": "Aceitar apenas respostas que atendam aos seguintes critérios",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} está sendo usado na lógica da pergunta {questionIndex}. Por favor, remova-o da lógica primeiro.",
+2
View File
@@ -1614,6 +1614,8 @@
"phone": "É um telefone válido",
"url": "É um URL válido"
},
"validation_logic_and": "E (todos devem passar)",
"validation_logic_or": "OU (pelo menos um deve passar)",
"validation_rules": "Regras de validação",
"validation_rules_description": "Aceitar apenas respostas que cumpram os seguintes critérios",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
+2
View File
@@ -1614,6 +1614,8 @@
"phone": "Este un număr de telefon valid",
"url": "Este un URL valid"
},
"validation_logic_and": "ȘI (toate trebuie să fie îndeplinite)",
"validation_logic_or": "SAU (cel puțin una trebuie să fie îndeplinită)",
"validation_rules": "Reguli de validare",
"validation_rules_description": "Acceptă doar răspunsurile care îndeplinesc următoarele criterii",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} este folosit în logica întrebării {questionIndex}. Vă rugăm să-l eliminați din logică mai întâi.",
+2
View File
@@ -1614,6 +1614,8 @@
"phone": "Корректный телефон",
"url": "Корректный URL"
},
"validation_logic_and": "И (все условия должны быть выполнены)",
"validation_logic_or": "ИЛИ (должно быть выполнено хотя бы одно условие)",
"validation_rules": "Правила валидации",
"validation_rules_description": "Принимать только ответы, соответствующие следующим критериям",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} используется в логике вопроса {questionIndex}. Пожалуйста, сначала удалите его из логики.",
+2
View File
@@ -1614,6 +1614,8 @@
"phone": "Är ett giltigt telefonnummer",
"url": "Är en giltig URL"
},
"validation_logic_and": "OCH (alla måste uppfyllas)",
"validation_logic_or": "ELLER (minst en måste uppfyllas)",
"validation_rules": "Valideringsregler",
"validation_rules_description": "Acceptera endast svar som uppfyller följande kriterier",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} används i logiken för fråga {questionIndex}. Vänligen ta bort den från logiken först.",
+2
View File
@@ -1614,6 +1614,8 @@
"phone": "是有效的手机号",
"url": "是有效的URL"
},
"validation_logic_and": "且(全部需通过)",
"validation_logic_or": "或(至少通过一项)",
"validation_rules": "校验规则",
"validation_rules_description": "仅接受符合以下条件的回复",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "\"{variable} 在 问题 {questionIndex} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
+2
View File
@@ -1614,6 +1614,8 @@
"phone": "是有效的電話號碼",
"url": "是有效的 URL"
},
"validation_logic_and": "AND(全部必須通過)",
"validation_logic_or": "OR(至少一項必須通過)",
"validation_rules": "驗證規則",
"validation_rules_description": "僅接受符合下列條件的回應",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "'{'variable'}' 用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
@@ -137,6 +137,12 @@ export const DateElementForm = ({
validationRules: rules,
});
}}
validationLogic={element.validationLogic}
onUpdateValidationLogic={(logic) => {
updateElement(elementIdx, {
validationLogic: logic,
});
}}
/>
</form>
);
@@ -414,6 +414,12 @@ export const MultipleChoiceElementForm = ({
});
}}
element={element}
validationLogic={element.validationLogic}
onUpdateValidationLogic={(logic) => {
updateElement(elementIdx, {
validationLogic: logic,
});
}}
/>
) : (
<ValidationRulesEditor
@@ -425,6 +431,12 @@ export const MultipleChoiceElementForm = ({
});
}}
element={element}
validationLogic={element.validationLogic}
onUpdateValidationLogic={(logic) => {
updateElement(elementIdx, {
validationLogic: logic,
});
}}
/>
)}
</form>
@@ -151,6 +151,12 @@ export const OpenElementForm = ({
validationRules: rules,
});
}}
validationLogic={element.validationLogic}
onUpdateValidationLogic={(logic) => {
updateElement(elementIdx, {
validationLogic: logic,
});
}}
/>
</div>
</form>
@@ -5,7 +5,7 @@ import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"
import { PlusIcon, TrashIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { v4 as uuidv7 } from "uuid";
import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
import { TSurveyElement, TSurveyElementTypeEnum, TValidationLogic } from "@formbricks/types/surveys/elements";
import { TValidationRule, TValidationRuleType } from "@formbricks/types/surveys/validation-rules";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
@@ -27,6 +27,8 @@ interface ValidationRulesEditorProps {
validationRules: TValidationRule[];
onUpdateRules: (rules: TValidationRule[]) => void;
element?: TSurveyElement; // Optional, needed for single select option selection
validationLogic?: TValidationLogic;
onUpdateValidationLogic?: (logic: TValidationLogic) => void;
}
export const ValidationRulesEditor = ({
@@ -34,6 +36,8 @@ export const ValidationRulesEditor = ({
validationRules,
onUpdateRules,
element,
validationLogic = "and",
onUpdateValidationLogic,
}: ValidationRulesEditorProps) => {
const { t } = useTranslation();
@@ -189,6 +193,22 @@ export const ValidationRulesEditor = ({
description={t("environments.surveys.edit.validation_rules_description")}
customContainerClass="p-0 mt-4"
childrenContainerClass="flex-col p-3 gap-2">
{/* Validation Logic Selector - only show when there are 2+ rules */}
{validationRules.length >= 2 && onUpdateValidationLogic && (
<div className="flex w-full items-center gap-2">
<Select
value={validationLogic}
onValueChange={(value) => onUpdateValidationLogic(value as TValidationLogic)}>
<SelectTrigger className="h-8 w-fit bg-white">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="and">{t("environments.surveys.edit.validation_logic_and")}</SelectItem>
<SelectItem value="or">{t("environments.surveys.edit.validation_logic_or")}</SelectItem>
</SelectContent>
</Select>
</div>
)}
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={validationRules.map((r) => r.id)} strategy={verticalListSortingStrategy}>
<div className="flex w-full flex-col gap-2">
@@ -203,28 +203,78 @@ export const validateElementResponse = (
// Then check validation rules
const rules: TValidationRule[] = [
...((element as TSurveyElement & { validationRules?: TValidationRule[] }).validationRules ?? []),
...((element as TSurveyElement & { validationRules?: TValidationRule[]; validationLogic?: "and" | "or" })
.validationRules ?? []),
];
for (const rule of rules) {
const ruleType = rule.type;
const validator = validators[ruleType];
if (rules.length === 0) {
return { valid: errors.length === 0, errors };
}
if (!validator) {
console.warn(`Unknown validation rule type: ${ruleType}`);
continue;
// Get validation logic (default to "and" if not specified)
const validationLogic =
(element as TSurveyElement & { validationLogic?: "and" | "or" }).validationLogic ?? "and";
if (validationLogic === "or") {
// OR logic: at least one rule must pass
const ruleResults: { valid: boolean; error?: TValidationError }[] = [];
for (const rule of rules) {
const ruleType = rule.type;
const validator = validators[ruleType];
if (!validator) {
console.warn(`Unknown validation rule type: ${ruleType}`);
continue;
}
const checkResult = validator.check(value, rule.params, element);
if (checkResult.valid) {
// At least one rule passed, validation succeeds
return { valid: errors.length === 0, errors };
} else {
// Rule failed, store the error
const message = getDefaultErrorMessage(rule, element, languageCode, t);
ruleResults.push({
valid: false,
error: {
ruleId: rule.id,
ruleType,
message,
},
});
}
}
const checkResult = validator.check(value, rule.params, element);
// All rules failed, add all errors
for (const result of ruleResults) {
if (result.error) {
errors.push(result.error);
}
}
} else {
// AND logic (default): all rules must pass
for (const rule of rules) {
const ruleType = rule.type;
const validator = validators[ruleType];
if (!checkResult.valid) {
const message = getDefaultErrorMessage(rule, element, languageCode, t);
if (!validator) {
console.warn(`Unknown validation rule type: ${ruleType}`);
continue;
}
errors.push({
ruleId: rule.id,
ruleType,
message,
});
const checkResult = validator.check(value, rule.params, element);
if (!checkResult.valid) {
const message = getDefaultErrorMessage(rule, element, languageCode, t);
errors.push({
ruleId: rule.id,
ruleType,
message,
});
}
}
}
+5
View File
@@ -65,6 +65,10 @@ export const ZSurveyElementId = z.string().superRefine((id, ctx) => {
export type TSurveyElementId = z.infer<typeof ZSurveyElementId>;
// Validation logic operator - determines how multiple validation rules are combined
export const ZValidationLogic = z.enum(["and", "or"]);
export type TValidationLogic = z.infer<typeof ZValidationLogic>;
// Base element (like ZSurveyQuestionBase but WITHOUT logic, buttonLabel, backButtonLabel)
// Note: validationRules is not included in base - each element type will add its own narrowed schema
export const ZSurveyElementBase = z.object({
@@ -78,6 +82,7 @@ export const ZSurveyElementBase = z.object({
scale: z.enum(["number", "smiley", "star"]).optional(),
range: z.union([z.literal(5), z.literal(3), z.literal(4), z.literal(7), z.literal(10)]).optional(),
isDraft: z.boolean().optional(),
validationLogic: ZValidationLogic.optional(), // "and" = all rules must pass, "or" = at least one must pass
});
// OpenText Element