mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-22 11:29:22 -05:00
Introduced validation logic options (and , or)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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} のロジックで使用されています。まず、ロジックから削除してください。",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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}. Пожалуйста, сначала удалите его из логики.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user