mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-07 18:48:49 -06:00
Compare commits
6 Commits
fix/server
...
feat/valid
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67161155a9 | ||
|
|
9b0cf5f532 | ||
|
|
a32241d7c8 | ||
|
|
a296ad189a | ||
|
|
942cb0f8d0 | ||
|
|
3e3b8cc349 |
@@ -211,7 +211,6 @@ checksums:
|
||||
common/imprint: c4e5f2a1994d3cc5896b200709cc499c
|
||||
common/in_progress: 3de9afebcb9d4ce8ac42e14995f79ffd
|
||||
common/inactive_surveys: 324b8e1844739cdc2a3bc71aef143a76
|
||||
common/input_type: df4865b5d0a598a8d7f563dcec104df5
|
||||
common/integration: 40d02f65c4356003e0e90ffb944907d2
|
||||
common/integrations: 0ccce343287704cd90150c32e2fcad36
|
||||
common/invalid_date: 4c18c82f7317d4a02f8d5fef611e82b7
|
||||
@@ -235,13 +234,11 @@ checksums:
|
||||
common/look_and_feel: 9125503712626d495cedec7a79f1418c
|
||||
common/manage: a3d40c0267b81ae53c9598eaeb05087d
|
||||
common/marketing: fcf0f06f8b64b458c7ca6d95541a3cc8
|
||||
common/maximum: 4c07541dd1f093775bdc61b559cca6c8
|
||||
common/member: 1606dc30b369856b9dba1fe9aec425d2
|
||||
common/members: 0932e80cba1e3e0a7f52bb67ff31da32
|
||||
common/members_and_teams: bf5c3fadcb9fc23533ec1532b805ac08
|
||||
common/membership_not_found: 7ac63584af23396aace9992ad919ffd4
|
||||
common/metadata: 695d4f7da261ba76e3be4de495491028
|
||||
common/minimum: d9759235086d0169928b3c1401115e22
|
||||
common/mobile_overlay_app_works_best_on_desktop: 4509f7bfbb4edbd42e534042d6cb7e72
|
||||
common/mobile_overlay_surveys_look_good: 6d73b635018b4a5a89cce58e1d2497f5
|
||||
common/mobile_overlay_title: 42f52259b7527989fb3a3240f5352a8b
|
||||
@@ -1149,8 +1146,6 @@ checksums:
|
||||
environments/surveys/edit/change_the_question_color_of_the_survey: ab6942138a8c5fc6c8c3b9f8dd95e980
|
||||
environments/surveys/edit/changes_saved: 90aab363c9e96eaa1295a997c48f97f6
|
||||
environments/surveys/edit/changing_survey_type_will_remove_existing_distribution_channels: 9ce817be04f13f2f0db981145ec48df4
|
||||
environments/surveys/edit/character_limit_toggle_description: d15a6895eaaf4d4c7212d9240c6bf45d
|
||||
environments/surveys/edit/character_limit_toggle_title: fdc45bcc6335e5116aec895fecda0d87
|
||||
environments/surveys/edit/checkbox_label: 12a07d6bdf38e283a2e95892ec49b7f8
|
||||
environments/surveys/edit/choose_the_actions_which_trigger_the_survey: 773b311a148a112243f3b139506b9987
|
||||
environments/surveys/edit/choose_the_first_question_on_your_block: bdece06ca04f89d0c445ba1554dd5b80
|
||||
@@ -1516,6 +1511,21 @@ checksums:
|
||||
environments/surveys/edit/upper_label: 1fa48bce3fade6ffc1a52d9fdddf9e17
|
||||
environments/surveys/edit/url_filters: e524879d2eb74463d7fd06a7e0f53421
|
||||
environments/surveys/edit/url_not_supported: af8a753467c617b596aadef1aaaed664
|
||||
environments/surveys/edit/validation/characters: f62970e214bd04fd1959e2759ee1ec48
|
||||
environments/surveys/edit/validation/email: a481cd9fba3e145252458ee1eaa9bd3b
|
||||
environments/surveys/edit/validation/max_length: dad68e07f6ee06ed11ec6bda2e896c68
|
||||
environments/surveys/edit/validation/max_selections: 6edf9e1149c3893da102d9464138da22
|
||||
environments/surveys/edit/validation/max_value: 6109d595ba21497c59b1c91d7fd09a13
|
||||
environments/surveys/edit/validation/min_length: ad5c57a937565826794fb865522962e8
|
||||
environments/surveys/edit/validation/min_selections: 204dbf1f1b3aa34c8b981642b1694262
|
||||
environments/surveys/edit/validation/min_value: b9542ab0e0ea0ee18e82931b160b1385
|
||||
environments/surveys/edit/validation/options_selected: 088309b017c07c01494447dba82b2621
|
||||
environments/surveys/edit/validation/pattern: c6f01d7bc9baa21a40ea38fa986bd5a0
|
||||
environments/surveys/edit/validation/phone: bcd7bd37a475ab1f80ea4c5b4d4d0bb5
|
||||
environments/surveys/edit/validation/required: b6c231d5d1a8dfe37615d1efd38ed8e0
|
||||
environments/surveys/edit/validation/url: 4006a4d8dfac013758f0053f6aa67cdd
|
||||
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
|
||||
environments/surveys/edit/variable_is_used_in_quota_please_remove_it_from_quota_first: 0d36e5b2713f5450fe346e0af0aaa29c
|
||||
environments/surveys/edit/variable_name_is_already_taken_please_choose_another: 6da42fe8733c6379158bce9a176f76d7
|
||||
|
||||
@@ -238,7 +238,6 @@
|
||||
"imprint": "Impressum",
|
||||
"in_progress": "Im Gange",
|
||||
"inactive_surveys": "Inaktive Umfragen",
|
||||
"input_type": "Eingabetyp",
|
||||
"integration": "Integration",
|
||||
"integrations": "Integrationen",
|
||||
"invalid_date": "Ungültiges Datum",
|
||||
@@ -262,13 +261,11 @@
|
||||
"look_and_feel": "Darstellung",
|
||||
"manage": "Verwalten",
|
||||
"marketing": "Marketing",
|
||||
"maximum": "Maximal",
|
||||
"member": "Mitglied",
|
||||
"members": "Mitglieder",
|
||||
"members_and_teams": "Mitglieder & Teams",
|
||||
"membership_not_found": "Mitgliedschaft nicht gefunden",
|
||||
"metadata": "Metadaten",
|
||||
"minimum": "Minimum",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks funktioniert am besten auf einem größeren Bildschirm. Um Umfragen zu verwalten oder zu erstellen, wechsle zu einem anderen Gerät.",
|
||||
"mobile_overlay_surveys_look_good": "Keine Sorge – deine Umfragen sehen auf jedem Gerät und jeder Bildschirmgröße großartig aus!",
|
||||
"mobile_overlay_title": "Oops, Bildschirm zu klein erkannt!",
|
||||
@@ -1220,8 +1217,6 @@
|
||||
"change_the_question_color_of_the_survey": "Fragefarbe der Umfrage ändern.",
|
||||
"changes_saved": "Änderungen gespeichert.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "\"Das Ändern des Umfragetypen beeinflusst, wie er geteilt werden kann. Wenn Teilnehmer bereits Zugriffslinks für den aktuellen Typ haben, könnten sie das Zugriffsrecht nach dem Wechsel verlieren.\"",
|
||||
"character_limit_toggle_description": "Begrenzen Sie, wie kurz oder lang eine Antwort sein kann.",
|
||||
"character_limit_toggle_title": "Fügen Sie Zeichenbeschränkungen hinzu",
|
||||
"checkbox_label": "Checkbox-Beschriftung",
|
||||
"choose_the_actions_which_trigger_the_survey": "Aktionen auswählen, die die Umfrage auslösen.",
|
||||
"choose_the_first_question_on_your_block": "Wählen sie die erste frage in ihrem block",
|
||||
@@ -1589,6 +1584,23 @@
|
||||
"upper_label": "Oberes Label",
|
||||
"url_filters": "URL-Filter",
|
||||
"url_not_supported": "URL nicht unterstützt",
|
||||
"validation": {
|
||||
"characters": "Zeichen",
|
||||
"email": "Ist gültige E-Mail",
|
||||
"max_length": "Ist kürzer als",
|
||||
"max_selections": "Höchstens",
|
||||
"max_value": "Ist weniger als",
|
||||
"min_length": "Ist länger als",
|
||||
"min_selections": "Mindestens",
|
||||
"min_value": "Ist größer als",
|
||||
"options_selected": "Optionen ausgewählt",
|
||||
"pattern": "Entspricht Regex-Muster",
|
||||
"phone": "Ist gültige Telefonnummer",
|
||||
"required": "Ist erforderlich",
|
||||
"url": "Ist gültige URL"
|
||||
},
|
||||
"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.",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variable \"{variableName}\" wird in der \"{quotaName}\" Quote verwendet",
|
||||
"variable_name_is_already_taken_please_choose_another": "Variablenname ist bereits vergeben, bitte wähle einen anderen.",
|
||||
|
||||
@@ -238,7 +238,6 @@
|
||||
"imprint": "Imprint",
|
||||
"in_progress": "In Progress",
|
||||
"inactive_surveys": "Inactive surveys",
|
||||
"input_type": "Input type",
|
||||
"integration": "integration",
|
||||
"integrations": "Integrations",
|
||||
"invalid_date": "Invalid date",
|
||||
@@ -262,13 +261,11 @@
|
||||
"look_and_feel": "Look & Feel",
|
||||
"manage": "Manage",
|
||||
"marketing": "Marketing",
|
||||
"maximum": "Maximum",
|
||||
"member": "Member",
|
||||
"members": "Members",
|
||||
"members_and_teams": "Members & Teams",
|
||||
"membership_not_found": "Membership not found",
|
||||
"metadata": "Metadata",
|
||||
"minimum": "Minimum",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks works best on a bigger screen. To manage or build surveys, switch to another device.",
|
||||
"mobile_overlay_surveys_look_good": "Don't worry – your surveys look great on every device and screen size!",
|
||||
"mobile_overlay_title": "Oops, tiny screen detected!",
|
||||
@@ -1220,8 +1217,6 @@
|
||||
"change_the_question_color_of_the_survey": "Change the question color of the survey.",
|
||||
"changes_saved": "Changes saved.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Changing the survey type will affect how it can be shared. If respondents already have access links for the current type, they may lose access after the switch.",
|
||||
"character_limit_toggle_description": "Limit how short or long an answer can be.",
|
||||
"character_limit_toggle_title": "Add character limits",
|
||||
"checkbox_label": "Checkbox Label",
|
||||
"choose_the_actions_which_trigger_the_survey": "Choose the actions which trigger the survey.",
|
||||
"choose_the_first_question_on_your_block": "Choose the first question on your Block",
|
||||
@@ -1589,6 +1584,23 @@
|
||||
"upper_label": "Upper Label",
|
||||
"url_filters": "URL Filters",
|
||||
"url_not_supported": "URL not supported",
|
||||
"validation": {
|
||||
"characters": "characters",
|
||||
"email": "Is valid email",
|
||||
"max_length": "Is shorter than",
|
||||
"max_selections": "At most",
|
||||
"max_value": "Is less than",
|
||||
"min_length": "Is longer than",
|
||||
"min_selections": "At least",
|
||||
"min_value": "Is greater than",
|
||||
"options_selected": "options selected",
|
||||
"pattern": "Matches regex pattern",
|
||||
"phone": "Is valid phone",
|
||||
"required": "Is required",
|
||||
"url": "Is valid URL"
|
||||
},
|
||||
"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.",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variable \"{variableName}\" is being used in \"{quotaName}\" quota",
|
||||
"variable_name_is_already_taken_please_choose_another": "Variable name is already taken, please choose another.",
|
||||
@@ -3034,4 +3046,4 @@
|
||||
"usability_rating_description": "Measure perceived usability by asking users to rate their experience with your product using a standardized 10-question survey.",
|
||||
"usability_score_name": "System Usability Score (SUS)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,7 +238,6 @@
|
||||
"imprint": "Aviso legal",
|
||||
"in_progress": "En progreso",
|
||||
"inactive_surveys": "Encuestas inactivas",
|
||||
"input_type": "Tipo de entrada",
|
||||
"integration": "integración",
|
||||
"integrations": "Integraciones",
|
||||
"invalid_date": "Fecha no válida",
|
||||
@@ -262,13 +261,11 @@
|
||||
"look_and_feel": "Apariencia",
|
||||
"manage": "Gestionar",
|
||||
"marketing": "Marketing",
|
||||
"maximum": "Máximo",
|
||||
"member": "Miembro",
|
||||
"members": "Miembros",
|
||||
"members_and_teams": "Miembros y equipos",
|
||||
"membership_not_found": "Membresía no encontrada",
|
||||
"metadata": "Metadatos",
|
||||
"minimum": "Mínimo",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks funciona mejor en una pantalla más grande. Para gestionar o crear encuestas, cambia a otro dispositivo.",
|
||||
"mobile_overlay_surveys_look_good": "No te preocupes – ¡tus encuestas se ven geniales en todos los dispositivos y tamaños de pantalla!",
|
||||
"mobile_overlay_title": "¡Ups, pantalla pequeña detectada!",
|
||||
@@ -1220,8 +1217,6 @@
|
||||
"change_the_question_color_of_the_survey": "Cambiar el color de las preguntas de la encuesta.",
|
||||
"changes_saved": "Cambios guardados.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Cambiar el tipo de encuesta afectará a cómo se puede compartir. Si los encuestados ya tienen enlaces de acceso para el tipo actual, podrían perder el acceso después del cambio.",
|
||||
"character_limit_toggle_description": "Limitar lo corta o larga que puede ser una respuesta.",
|
||||
"character_limit_toggle_title": "Añadir límites de caracteres",
|
||||
"checkbox_label": "Etiqueta de casilla de verificación",
|
||||
"choose_the_actions_which_trigger_the_survey": "Elige las acciones que activan la encuesta.",
|
||||
"choose_the_first_question_on_your_block": "Elige la primera pregunta en tu bloque",
|
||||
@@ -1589,6 +1584,23 @@
|
||||
"upper_label": "Etiqueta superior",
|
||||
"url_filters": "Filtros de URL",
|
||||
"url_not_supported": "URL no compatible",
|
||||
"validation": {
|
||||
"characters": "caracteres",
|
||||
"email": "Es un correo electrónico válido",
|
||||
"max_length": "Es más corto que",
|
||||
"max_selections": "Como máximo",
|
||||
"max_value": "Es menor que",
|
||||
"min_length": "Es más largo que",
|
||||
"min_selections": "Al menos",
|
||||
"min_value": "Es mayor que",
|
||||
"options_selected": "opciones seleccionadas",
|
||||
"pattern": "Coincide con el patrón regex",
|
||||
"phone": "Es un teléfono válido",
|
||||
"required": "Es obligatorio",
|
||||
"url": "Es una URL válida"
|
||||
},
|
||||
"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.",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "La variable \"{variableName}\" se está utilizando en la cuota \"{quotaName}\"",
|
||||
"variable_name_is_already_taken_please_choose_another": "El nombre de la variable ya está en uso, por favor elige otro.",
|
||||
|
||||
@@ -238,7 +238,6 @@
|
||||
"imprint": "Empreinte",
|
||||
"in_progress": "En cours",
|
||||
"inactive_surveys": "Sondages inactifs",
|
||||
"input_type": "Type d'entrée",
|
||||
"integration": "intégration",
|
||||
"integrations": "Intégrations",
|
||||
"invalid_date": "Date invalide",
|
||||
@@ -262,13 +261,11 @@
|
||||
"look_and_feel": "Apparence",
|
||||
"manage": "Gérer",
|
||||
"marketing": "Marketing",
|
||||
"maximum": "Max",
|
||||
"member": "Membre",
|
||||
"members": "Membres",
|
||||
"members_and_teams": "Membres & Équipes",
|
||||
"membership_not_found": "Abonnement non trouvé",
|
||||
"metadata": "Métadonnées",
|
||||
"minimum": "Min",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks fonctionne mieux sur un écran plus grand. Pour gérer ou créer des sondages, passez à un autre appareil.",
|
||||
"mobile_overlay_surveys_look_good": "Ne t'inquiète pas – tes enquêtes sont superbes sur tous les appareils et tailles d'écran!",
|
||||
"mobile_overlay_title": "Oups, écran minuscule détecté!",
|
||||
@@ -1220,8 +1217,6 @@
|
||||
"change_the_question_color_of_the_survey": "Vous pouvez modifier la couleur des questions d'une enquête.",
|
||||
"changes_saved": "Modifications enregistrées.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Le changement du type de sondage affectera la façon dont il peut être partagé. Si les répondants ont déjà des liens d'accès pour le type actuel, ils peuvent perdre l'accès après le changement.",
|
||||
"character_limit_toggle_description": "Limitez la longueur des réponses.",
|
||||
"character_limit_toggle_title": "Ajouter des limites de caractères",
|
||||
"checkbox_label": "Étiquette de case à cocher",
|
||||
"choose_the_actions_which_trigger_the_survey": "Choisissez les actions qui déclenchent l'enquête.",
|
||||
"choose_the_first_question_on_your_block": "Choisissez la première question de votre bloc",
|
||||
@@ -1589,6 +1584,23 @@
|
||||
"upper_label": "Étiquette supérieure",
|
||||
"url_filters": "Filtres d'URL",
|
||||
"url_not_supported": "URL non supportée",
|
||||
"validation": {
|
||||
"characters": "caractères",
|
||||
"email": "Est un e-mail valide",
|
||||
"max_length": "Est plus court que",
|
||||
"max_selections": "Au maximum",
|
||||
"max_value": "Est inférieur à",
|
||||
"min_length": "Est plus long que",
|
||||
"min_selections": "Au moins",
|
||||
"min_value": "Est supérieur à",
|
||||
"options_selected": "options sélectionnées",
|
||||
"pattern": "Correspond au modèle d'expression régulière",
|
||||
"phone": "Est un numéro de téléphone valide",
|
||||
"required": "Est requis",
|
||||
"url": "Est une URL valide"
|
||||
},
|
||||
"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.",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "La variable \"{variableName}\" est utilisée dans le quota \"{quotaName}\"",
|
||||
"variable_name_is_already_taken_please_choose_another": "Le nom de la variable est déjà pris, veuillez en choisir un autre.",
|
||||
|
||||
@@ -238,7 +238,6 @@
|
||||
"imprint": "企業情報",
|
||||
"in_progress": "進行中",
|
||||
"inactive_surveys": "非アクティブなフォーム",
|
||||
"input_type": "入力タイプ",
|
||||
"integration": "連携",
|
||||
"integrations": "連携",
|
||||
"invalid_date": "無効な日付です",
|
||||
@@ -262,13 +261,11 @@
|
||||
"look_and_feel": "デザイン",
|
||||
"manage": "管理",
|
||||
"marketing": "マーケティング",
|
||||
"maximum": "最大",
|
||||
"member": "メンバー",
|
||||
"members": "メンバー",
|
||||
"members_and_teams": "メンバー&チーム",
|
||||
"membership_not_found": "メンバーシップが見つかりません",
|
||||
"metadata": "メタデータ",
|
||||
"minimum": "最小",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks は より 大きな 画面 で最適に 作動します。 フォーム を 管理または 構築する には、 別の デバイス に 切り替える 必要が あります。",
|
||||
"mobile_overlay_surveys_look_good": "ご安心ください - お使い の デバイス や 画面 サイズ に 関係なく、 フォーム は 素晴らしく 見えます!",
|
||||
"mobile_overlay_title": "おっと、 小さな 画面 が 検出されました!",
|
||||
@@ -1220,8 +1217,6 @@
|
||||
"change_the_question_color_of_the_survey": "フォームの質問の色を変更します。",
|
||||
"changes_saved": "変更を保存しました。",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "フォームの種類を変更すると、共有方法に影響します。回答者が現在のタイプのアクセスリンクをすでに持っている場合、切り替え後にアクセスを失う可能性があります。",
|
||||
"character_limit_toggle_description": "回答の長さの上限・下限を設定します。",
|
||||
"character_limit_toggle_title": "文字数制限を追加",
|
||||
"checkbox_label": "チェックボックスのラベル",
|
||||
"choose_the_actions_which_trigger_the_survey": "フォームをトリガーするアクションを選択してください。",
|
||||
"choose_the_first_question_on_your_block": "ブロックの最初の質問を選択してください",
|
||||
@@ -1589,6 +1584,23 @@
|
||||
"upper_label": "上限ラベル",
|
||||
"url_filters": "URLフィルター",
|
||||
"url_not_supported": "URLはサポートされていません",
|
||||
"validation": {
|
||||
"characters": "文字",
|
||||
"email": "有効なメールアドレスである",
|
||||
"max_length": "より短い",
|
||||
"max_selections": "最大",
|
||||
"max_value": "より小さい",
|
||||
"min_length": "より長い",
|
||||
"min_selections": "最小",
|
||||
"min_value": "より大きい",
|
||||
"options_selected": "個のオプションが選択されている",
|
||||
"pattern": "正規表現パターンに一致する",
|
||||
"phone": "有効な電話番号である",
|
||||
"required": "必須である",
|
||||
"url": "有効なURLである"
|
||||
},
|
||||
"validation_rules": "検証ルール",
|
||||
"validation_rules_description": "次の条件を満たす回答のみを受け付ける",
|
||||
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} は質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "変数 \"{variableName}\" は \"{quotaName}\" クォータ で使用されています",
|
||||
"variable_name_is_already_taken_please_choose_another": "変数名はすでに使用されています。別の名前を選択してください。",
|
||||
|
||||
@@ -238,7 +238,6 @@
|
||||
"imprint": "Afdruk",
|
||||
"in_progress": "In uitvoering",
|
||||
"inactive_surveys": "Inactieve enquêtes",
|
||||
"input_type": "Invoertype",
|
||||
"integration": "integratie",
|
||||
"integrations": "Integraties",
|
||||
"invalid_date": "Ongeldige datum",
|
||||
@@ -262,13 +261,11 @@
|
||||
"look_and_feel": "Kijk & voel",
|
||||
"manage": "Beheren",
|
||||
"marketing": "Marketing",
|
||||
"maximum": "Maximaal",
|
||||
"member": "Lid",
|
||||
"members": "Leden",
|
||||
"members_and_teams": "Leden & teams",
|
||||
"membership_not_found": "Lidmaatschap niet gevonden",
|
||||
"metadata": "Metagegevens",
|
||||
"minimum": "Minimum",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks werkt het beste op een groter scherm. Schakel over naar een ander apparaat om enquêtes te beheren of samen te stellen.",
|
||||
"mobile_overlay_surveys_look_good": "Maakt u zich geen zorgen: uw enquêtes zien er geweldig uit op elk apparaat en schermformaat!",
|
||||
"mobile_overlay_title": "Oeps, klein scherm gedetecteerd!",
|
||||
@@ -1220,8 +1217,6 @@
|
||||
"change_the_question_color_of_the_survey": "Verander de vraagkleur van de enquête.",
|
||||
"changes_saved": "Wijzigingen opgeslagen.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Het wijzigen van het enquêtetype heeft invloed op de manier waarop deze kan worden gedeeld. Als respondenten al toegangslinks hebben voor het huidige type, verliezen ze mogelijk de toegang na de overstap.",
|
||||
"character_limit_toggle_description": "Beperk hoe kort of lang een antwoord mag zijn.",
|
||||
"character_limit_toggle_title": "Tekenlimieten toevoegen",
|
||||
"checkbox_label": "Selectievakje-label",
|
||||
"choose_the_actions_which_trigger_the_survey": "Kies de acties die de enquête activeren.",
|
||||
"choose_the_first_question_on_your_block": "Kies de eerste vraag in je blok",
|
||||
@@ -1589,6 +1584,23 @@
|
||||
"upper_label": "Bovenste etiket",
|
||||
"url_filters": "URL-filters",
|
||||
"url_not_supported": "URL niet ondersteund",
|
||||
"validation": {
|
||||
"characters": "tekens",
|
||||
"email": "Is geldig e-mailadres",
|
||||
"max_length": "Is korter dan",
|
||||
"max_selections": "Maximaal",
|
||||
"max_value": "Is minder dan",
|
||||
"min_length": "Is langer dan",
|
||||
"min_selections": "Minimaal",
|
||||
"min_value": "Is groter dan",
|
||||
"options_selected": "opties geselecteerd",
|
||||
"pattern": "Komt overeen met regex-patroon",
|
||||
"phone": "Is geldig telefoonnummer",
|
||||
"required": "Is verplicht",
|
||||
"url": "Is geldige URL"
|
||||
},
|
||||
"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.",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variabele \"{variableName}\" wordt gebruikt in het \"{quotaName}\" quotum",
|
||||
"variable_name_is_already_taken_please_choose_another": "Variabelenaam is al in gebruik, kies een andere.",
|
||||
|
||||
@@ -238,7 +238,6 @@
|
||||
"imprint": "impressão",
|
||||
"in_progress": "Em andamento",
|
||||
"inactive_surveys": "Pesquisas inativas",
|
||||
"input_type": "Tipo de entrada",
|
||||
"integration": "integração",
|
||||
"integrations": "Integrações",
|
||||
"invalid_date": "Data inválida",
|
||||
@@ -262,13 +261,11 @@
|
||||
"look_and_feel": "Aparência e Experiência",
|
||||
"manage": "gerenciar",
|
||||
"marketing": "marketing",
|
||||
"maximum": "Máximo",
|
||||
"member": "Membros",
|
||||
"members": "Membros",
|
||||
"members_and_teams": "Membros e equipes",
|
||||
"membership_not_found": "Assinatura não encontrada",
|
||||
"metadata": "metadados",
|
||||
"minimum": "Mínimo",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks funciona melhor em uma tela maior. Para gerenciar ou criar pesquisas, mude para outro dispositivo.",
|
||||
"mobile_overlay_surveys_look_good": "Não se preocupe – suas pesquisas ficam ótimas em qualquer dispositivo e tamanho de tela!",
|
||||
"mobile_overlay_title": "Eita, tela pequena detectada!",
|
||||
@@ -1220,8 +1217,6 @@
|
||||
"change_the_question_color_of_the_survey": "Muda a cor da pergunta da pesquisa.",
|
||||
"changes_saved": "Mudanças salvas.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Alterar o tipo de pesquisa afetará a forma como ela pode ser compartilhada. Se os respondentes já tiverem links de acesso para o tipo atual, podem perder o acesso após a mudança.",
|
||||
"character_limit_toggle_description": "Limite o quão curta ou longa uma resposta pode ser.",
|
||||
"character_limit_toggle_title": "Adicionar limites de caracteres",
|
||||
"checkbox_label": "Rótulo da Caixa de Seleção",
|
||||
"choose_the_actions_which_trigger_the_survey": "Escolha as ações que disparam a pesquisa.",
|
||||
"choose_the_first_question_on_your_block": "Escolha a primeira pergunta do seu bloco",
|
||||
@@ -1589,6 +1584,23 @@
|
||||
"upper_label": "Etiqueta Superior",
|
||||
"url_filters": "Filtros de URL",
|
||||
"url_not_supported": "URL não suportada",
|
||||
"validation": {
|
||||
"characters": "caracteres",
|
||||
"email": "É um e-mail válido",
|
||||
"max_length": "É menor que",
|
||||
"max_selections": "No máximo",
|
||||
"max_value": "É menor que",
|
||||
"min_length": "É maior que",
|
||||
"min_selections": "No mínimo",
|
||||
"min_value": "É maior que",
|
||||
"options_selected": "opções selecionadas",
|
||||
"pattern": "Corresponde ao padrão regex",
|
||||
"phone": "É um telefone válido",
|
||||
"required": "É obrigatório",
|
||||
"url": "É uma URL válida"
|
||||
},
|
||||
"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.",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variável \"{variableName}\" está sendo usada na cota \"{quotaName}\"",
|
||||
"variable_name_is_already_taken_please_choose_another": "O nome da variável já está em uso, por favor escolha outro.",
|
||||
|
||||
@@ -238,7 +238,6 @@
|
||||
"imprint": "Impressão",
|
||||
"in_progress": "Em Progresso",
|
||||
"inactive_surveys": "Inquéritos inativos",
|
||||
"input_type": "Tipo de entrada",
|
||||
"integration": "integração",
|
||||
"integrations": "Integrações",
|
||||
"invalid_date": "Data inválida",
|
||||
@@ -262,13 +261,11 @@
|
||||
"look_and_feel": "Aparência e Sensação",
|
||||
"manage": "Gerir",
|
||||
"marketing": "Marketing",
|
||||
"maximum": "Máximo",
|
||||
"member": "Membro",
|
||||
"members": "Membros",
|
||||
"members_and_teams": "Membros e equipas",
|
||||
"membership_not_found": "Associação não encontrada",
|
||||
"metadata": "Metadados",
|
||||
"minimum": "Mínimo",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks funciona melhor num ecrã maior. Para gerir ou criar inquéritos, mude de dispositivo.",
|
||||
"mobile_overlay_surveys_look_good": "Não se preocupe – os seus inquéritos têm uma ótima aparência em todos os dispositivos e tamanhos de ecrã!",
|
||||
"mobile_overlay_title": "Oops, ecrã pequeno detectado!",
|
||||
@@ -1220,8 +1217,6 @@
|
||||
"change_the_question_color_of_the_survey": "Alterar a cor da pergunta do inquérito",
|
||||
"changes_saved": "Alterações guardadas.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Alterar o tipo de inquérito afetará como ele pode ser partilhado. Se os respondentes já tiverem links de acesso para o tipo atual, podem perder o acesso após a mudança.",
|
||||
"character_limit_toggle_description": "Limitar o quão curta ou longa uma resposta pode ser.",
|
||||
"character_limit_toggle_title": "Adicionar limites de caracteres",
|
||||
"checkbox_label": "Rótulo da Caixa de Seleção",
|
||||
"choose_the_actions_which_trigger_the_survey": "Escolha as ações que desencadeiam o inquérito.",
|
||||
"choose_the_first_question_on_your_block": "Escolha a primeira pergunta no seu bloco",
|
||||
@@ -1589,6 +1584,23 @@
|
||||
"upper_label": "Etiqueta Superior",
|
||||
"url_filters": "Filtros de URL",
|
||||
"url_not_supported": "URL não suportado",
|
||||
"validation": {
|
||||
"characters": "caracteres",
|
||||
"email": "É um email válido",
|
||||
"max_length": "É mais curto que",
|
||||
"max_selections": "No máximo",
|
||||
"max_value": "É menos que",
|
||||
"min_length": "É mais longo que",
|
||||
"min_selections": "Pelo menos",
|
||||
"min_value": "É maior que",
|
||||
"options_selected": "opções selecionadas",
|
||||
"pattern": "Coincide com o padrão regex",
|
||||
"phone": "É um telefone válido",
|
||||
"required": "É obrigatório",
|
||||
"url": "É um URL válido"
|
||||
},
|
||||
"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.",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variável \"{variableName}\" está a ser utilizada na quota \"{quotaName}\"",
|
||||
"variable_name_is_already_taken_please_choose_another": "O nome da variável já está em uso, por favor escolha outro.",
|
||||
|
||||
@@ -238,7 +238,6 @@
|
||||
"imprint": "Amprentă",
|
||||
"in_progress": "În progres",
|
||||
"inactive_surveys": "Sondaje inactive",
|
||||
"input_type": "Tipul de intrare",
|
||||
"integration": "integrare",
|
||||
"integrations": "Integrări",
|
||||
"invalid_date": "Dată invalidă",
|
||||
@@ -262,13 +261,11 @@
|
||||
"look_and_feel": "Aspect și Comportament",
|
||||
"manage": "Gestionați",
|
||||
"marketing": "Marketing",
|
||||
"maximum": "Maximum",
|
||||
"member": "Membru",
|
||||
"members": "Membri",
|
||||
"members_and_teams": "Membri și echipe",
|
||||
"membership_not_found": "Apartenența nu a fost găsită",
|
||||
"metadata": "Metadate",
|
||||
"minimum": "Minim",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks funcționează cel mai bine pe un ecran mai mare. Pentru a gestiona sau crea chestionare, treceți la un alt dispozitiv.",
|
||||
"mobile_overlay_surveys_look_good": "Nu vă faceți griji – chestionarele dumneavoastră arată grozav pe orice dispozitiv și dimensiune a ecranului!",
|
||||
"mobile_overlay_title": "Ups, ecran mic detectat!",
|
||||
@@ -1220,8 +1217,6 @@
|
||||
"change_the_question_color_of_the_survey": "Schimbați culoarea întrebării chestionarului.",
|
||||
"changes_saved": "Modificările au fost salvate",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Schimbarea tipului chestionarului va afecta modul în care acesta poate fi distribuit. Dacă respondenții au deja linkuri de acces pentru tipul curent, aceștia ar putea pierde accesul după schimbare.",
|
||||
"character_limit_toggle_description": "Limitați cât de scurt sau lung poate fi un răspuns.",
|
||||
"character_limit_toggle_title": "Adăugați limite de caractere",
|
||||
"checkbox_label": "Etichetă casetă de selectare",
|
||||
"choose_the_actions_which_trigger_the_survey": "Alegeți acțiunile care declanșează sondajul.",
|
||||
"choose_the_first_question_on_your_block": "Alege prima întrebare din blocul tău",
|
||||
@@ -1589,6 +1584,23 @@
|
||||
"upper_label": "Etichetă superioară",
|
||||
"url_filters": "Filtre URL",
|
||||
"url_not_supported": "URL nesuportat",
|
||||
"validation": {
|
||||
"characters": "caractere",
|
||||
"email": "Este un email valid",
|
||||
"max_length": "Este mai scurt de",
|
||||
"max_selections": "Cel mult",
|
||||
"max_value": "Este mai mic decât",
|
||||
"min_length": "Este mai lung de",
|
||||
"min_selections": "Cel puțin",
|
||||
"min_value": "Este mai mare decât",
|
||||
"options_selected": "opțiuni selectate",
|
||||
"pattern": "Se potrivește cu un șablon regex",
|
||||
"phone": "Este un număr de telefon valid",
|
||||
"required": "Este obligatoriu",
|
||||
"url": "Este un URL valid"
|
||||
},
|
||||
"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.",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variabila \"{variableName}\" este folosită în cota \"{quotaName}\"",
|
||||
"variable_name_is_already_taken_please_choose_another": "Numele variabilei este deja utilizat, vă rugăm să alegeți altul.",
|
||||
|
||||
@@ -238,7 +238,6 @@
|
||||
"imprint": "Выходные данные",
|
||||
"in_progress": "В процессе",
|
||||
"inactive_surveys": "Неактивные опросы",
|
||||
"input_type": "Тип ввода",
|
||||
"integration": "интеграция",
|
||||
"integrations": "Интеграции",
|
||||
"invalid_date": "Неверная дата",
|
||||
@@ -262,13 +261,11 @@
|
||||
"look_and_feel": "Внешний вид",
|
||||
"manage": "Управление",
|
||||
"marketing": "Маркетинг",
|
||||
"maximum": "Максимум",
|
||||
"member": "Участник",
|
||||
"members": "Участники",
|
||||
"members_and_teams": "Участники и команды",
|
||||
"membership_not_found": "Участие не найдено",
|
||||
"metadata": "Метаданные",
|
||||
"minimum": "Минимум",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks лучше всего работает на большом экране. Для управления или создания опросов перейдите на другое устройство.",
|
||||
"mobile_overlay_surveys_look_good": "Не волнуйтесь — ваши опросы отлично выглядят на любом устройстве и экране!",
|
||||
"mobile_overlay_title": "Ой, обнаружен маленький экран!",
|
||||
@@ -1220,8 +1217,6 @@
|
||||
"change_the_question_color_of_the_survey": "Изменить цвет вопросов в опросе.",
|
||||
"changes_saved": "Изменения сохранены.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Изменение типа опроса повлияет на способы его распространения. Если у респондентов уже есть ссылки для доступа к текущему типу, после смены они могут потерять доступ.",
|
||||
"character_limit_toggle_description": "Ограничьте минимальную и максимальную длину ответа.",
|
||||
"character_limit_toggle_title": "Добавить ограничения на количество символов",
|
||||
"checkbox_label": "Метка флажка",
|
||||
"choose_the_actions_which_trigger_the_survey": "Выберите действия, которые запускают опрос.",
|
||||
"choose_the_first_question_on_your_block": "Выберите первый вопрос в вашем блоке",
|
||||
@@ -1589,6 +1584,23 @@
|
||||
"upper_label": "Верхняя метка",
|
||||
"url_filters": "Фильтры URL",
|
||||
"url_not_supported": "URL не поддерживается",
|
||||
"validation": {
|
||||
"characters": "символов",
|
||||
"email": "Корректный email",
|
||||
"max_length": "Короче чем",
|
||||
"max_selections": "Не более",
|
||||
"max_value": "Меньше чем",
|
||||
"min_length": "Длиннее чем",
|
||||
"min_selections": "Не менее",
|
||||
"min_value": "Больше чем",
|
||||
"options_selected": "выбрано вариантов",
|
||||
"pattern": "Соответствует шаблону regex",
|
||||
"phone": "Корректный телефон",
|
||||
"required": "Обязательное поле",
|
||||
"url": "Корректный URL"
|
||||
},
|
||||
"validation_rules": "Правила валидации",
|
||||
"validation_rules_description": "Принимать только ответы, соответствующие следующим критериям",
|
||||
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} используется в логике вопроса {questionIndex}. Пожалуйста, сначала удалите его из логики.",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Переменная «{variableName}» используется в квоте «{quotaName}»",
|
||||
"variable_name_is_already_taken_please_choose_another": "Это имя переменной уже занято, выберите другое.",
|
||||
|
||||
@@ -238,7 +238,6 @@
|
||||
"imprint": "Impressum",
|
||||
"in_progress": "Pågående",
|
||||
"inactive_surveys": "Inaktiva enkäter",
|
||||
"input_type": "Inmatningstyp",
|
||||
"integration": "integration",
|
||||
"integrations": "Integrationer",
|
||||
"invalid_date": "Ogiltigt datum",
|
||||
@@ -262,13 +261,11 @@
|
||||
"look_and_feel": "Utseende",
|
||||
"manage": "Hantera",
|
||||
"marketing": "Marknadsföring",
|
||||
"maximum": "Maximum",
|
||||
"member": "Medlem",
|
||||
"members": "Medlemmar",
|
||||
"members_and_teams": "Medlemmar och team",
|
||||
"membership_not_found": "Medlemskap hittades inte",
|
||||
"metadata": "Metadata",
|
||||
"minimum": "Minimum",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks fungerar bäst på en större skärm. Byt till en annan enhet för att hantera eller bygga enkäter.",
|
||||
"mobile_overlay_surveys_look_good": "Oroa dig inte – dina enkäter ser bra ut på alla enheter och skärmstorlekar!",
|
||||
"mobile_overlay_title": "Hoppsan, liten skärm upptäckt!",
|
||||
@@ -1220,8 +1217,6 @@
|
||||
"change_the_question_color_of_the_survey": "Ändra enkätens frågefärg.",
|
||||
"changes_saved": "Ändringar sparade.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Att ändra enkättypen påverkar hur den kan delas. Om respondenter redan har åtkomstlänkar för den nuvarande typen kan de förlora åtkomst efter bytet.",
|
||||
"character_limit_toggle_description": "Begränsa hur kort eller långt ett svar kan vara.",
|
||||
"character_limit_toggle_title": "Lägg till teckengränser",
|
||||
"checkbox_label": "Kryssruteetikett",
|
||||
"choose_the_actions_which_trigger_the_survey": "Välj de åtgärder som utlöser enkäten.",
|
||||
"choose_the_first_question_on_your_block": "Välj den första frågan i ditt block",
|
||||
@@ -1589,6 +1584,23 @@
|
||||
"upper_label": "Övre etikett",
|
||||
"url_filters": "URL-filter",
|
||||
"url_not_supported": "URL stöds inte",
|
||||
"validation": {
|
||||
"characters": "tecken",
|
||||
"email": "Är en giltig e-postadress",
|
||||
"max_length": "Är kortare än",
|
||||
"max_selections": "Högst",
|
||||
"max_value": "Är mindre än",
|
||||
"min_length": "Är längre än",
|
||||
"min_selections": "Minst",
|
||||
"min_value": "Är större än",
|
||||
"options_selected": "valda alternativ",
|
||||
"pattern": "Matchar regexmönster",
|
||||
"phone": "Är ett giltigt telefonnummer",
|
||||
"required": "Är obligatorisk",
|
||||
"url": "Är en giltig URL"
|
||||
},
|
||||
"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.",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variabel \"{variableName}\" används i kvoten \"{quotaName}\"",
|
||||
"variable_name_is_already_taken_please_choose_another": "Variabelnamnet är redan taget, vänligen välj ett annat.",
|
||||
|
||||
@@ -238,7 +238,6 @@
|
||||
"imprint": "印记",
|
||||
"in_progress": "进行中",
|
||||
"inactive_surveys": "不 活跃 调查",
|
||||
"input_type": "输入类型",
|
||||
"integration": "集成",
|
||||
"integrations": "集成",
|
||||
"invalid_date": "无效 日期",
|
||||
@@ -262,13 +261,11 @@
|
||||
"look_and_feel": "外观 & 感觉",
|
||||
"manage": "管理",
|
||||
"marketing": "市场营销",
|
||||
"maximum": "最大值",
|
||||
"member": "成员",
|
||||
"members": "成员",
|
||||
"members_and_teams": "成员和团队",
|
||||
"membership_not_found": "未找到会员资格",
|
||||
"metadata": "元数据",
|
||||
"minimum": "最低",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks 在 更大 的 屏幕 上 效果 最佳。 若 需要 管理 或 构建 调查, 请 切换 到 其他 设备。",
|
||||
"mobile_overlay_surveys_look_good": "别 担心 – 您 的 调查 在 每 一 种 设备 和 屏幕 尺寸 上 看起来 都 很 棒!",
|
||||
"mobile_overlay_title": "噢, 检测 到 小 屏幕!",
|
||||
@@ -1220,8 +1217,6 @@
|
||||
"change_the_question_color_of_the_survey": "更改调查的 问题颜色",
|
||||
"changes_saved": "更改 已 保存",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "更改 调查 类型 会影 响 分享 方式 。 如果 受访者 已经 拥有 当前 类型 的 访问 链接 , 在 更改 之后 ,他们 可能 会 失去 访问 权限 。",
|
||||
"character_limit_toggle_description": "限制 答案的短或长程度。",
|
||||
"character_limit_toggle_title": "添加 字符限制",
|
||||
"checkbox_label": "复选框 标签",
|
||||
"choose_the_actions_which_trigger_the_survey": "选择 触发 调查 的 动作 。",
|
||||
"choose_the_first_question_on_your_block": "选择区块中的第一个问题",
|
||||
@@ -1589,6 +1584,23 @@
|
||||
"upper_label": "上限标签",
|
||||
"url_filters": "URL 过滤器",
|
||||
"url_not_supported": "URL 不支持",
|
||||
"validation": {
|
||||
"characters": "个字符",
|
||||
"email": "是有效的邮箱地址",
|
||||
"max_length": "短于",
|
||||
"max_selections": "最多",
|
||||
"max_value": "小于",
|
||||
"min_length": "长于",
|
||||
"min_selections": "至少",
|
||||
"min_value": "大于",
|
||||
"options_selected": "项已选择",
|
||||
"pattern": "匹配正则表达式模式",
|
||||
"phone": "是有效的手机号",
|
||||
"required": "为必填项",
|
||||
"url": "是有效的URL"
|
||||
},
|
||||
"validation_rules": "校验规则",
|
||||
"validation_rules_description": "仅接受符合以下条件的回复",
|
||||
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "\"{variable} 在 问题 {questionIndex} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "变量 \"{variableName}\" 正在 被 \"{quotaName}\" 配额 使用",
|
||||
"variable_name_is_already_taken_please_choose_another": "变量名已被占用,请选择其他。",
|
||||
|
||||
@@ -238,7 +238,6 @@
|
||||
"imprint": "版本訊息",
|
||||
"in_progress": "進行中",
|
||||
"inactive_surveys": "停用中的問卷",
|
||||
"input_type": "輸入類型",
|
||||
"integration": "整合",
|
||||
"integrations": "整合",
|
||||
"invalid_date": "無效日期",
|
||||
@@ -262,13 +261,11 @@
|
||||
"look_and_feel": "外觀與風格",
|
||||
"manage": "管理",
|
||||
"marketing": "行銷",
|
||||
"maximum": "最大值",
|
||||
"member": "成員",
|
||||
"members": "成員",
|
||||
"members_and_teams": "成員與團隊",
|
||||
"membership_not_found": "找不到成員資格",
|
||||
"metadata": "元數據",
|
||||
"minimum": "最小值",
|
||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks 適合在大螢幕上使用。若要管理或建立問卷,請切換到其他裝置。",
|
||||
"mobile_overlay_surveys_look_good": "別擔心 -你的 問卷 在每個 裝置 和 螢幕尺寸 上 都 很出色!",
|
||||
"mobile_overlay_title": "糟糕 ,偵測到小螢幕!",
|
||||
@@ -1220,8 +1217,6 @@
|
||||
"change_the_question_color_of_the_survey": "變更問卷的問題顏色。",
|
||||
"changes_saved": "已儲存變更。",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "更改問卷類型會影響其共享方式。如果受訪者已擁有當前類型的存取連結,則在切換後可能會失去存取權限。",
|
||||
"character_limit_toggle_description": "限制答案的長度或短度。",
|
||||
"character_limit_toggle_title": "新增字元限制",
|
||||
"checkbox_label": "核取方塊標籤",
|
||||
"choose_the_actions_which_trigger_the_survey": "選擇觸發問卷的操作。",
|
||||
"choose_the_first_question_on_your_block": "選擇此區塊的第一個問題",
|
||||
@@ -1589,6 +1584,23 @@
|
||||
"upper_label": "上標籤",
|
||||
"url_filters": "網址篩選器",
|
||||
"url_not_supported": "不支援網址",
|
||||
"validation": {
|
||||
"characters": "個字元",
|
||||
"email": "是有效的電子郵件",
|
||||
"max_length": "少於",
|
||||
"max_selections": "最多",
|
||||
"max_value": "小於",
|
||||
"min_length": "多於",
|
||||
"min_selections": "至少",
|
||||
"min_value": "大於",
|
||||
"options_selected": "個選項已選",
|
||||
"pattern": "符合正則表達式樣式",
|
||||
"phone": "是有效的電話號碼",
|
||||
"required": "為必填",
|
||||
"url": "是有效的 URL"
|
||||
},
|
||||
"validation_rules": "驗證規則",
|
||||
"validation_rules_description": "僅接受符合下列條件的回應",
|
||||
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "'{'variable'}' 用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
|
||||
"variable_is_used_in_quota_please_remove_it_from_quota_first": "變數 \"{variableName}\" 正被使用於 \"{quotaName}\" 配額中",
|
||||
"variable_name_is_already_taken_please_choose_another": "已使用此變數名稱,請選擇另一個名稱。",
|
||||
|
||||
@@ -9,7 +9,6 @@ import { type TI18nString } from "@formbricks/types/i18n";
|
||||
import {
|
||||
TSurveyElement,
|
||||
TSurveyElementChoice,
|
||||
TSurveyElementTypeEnum,
|
||||
} from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey, TSurveyEndScreenCard, TSurveyRedirectUrlCard } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
@@ -24,7 +23,6 @@ import { Button } from "@/modules/ui/components/button";
|
||||
import { FileInput } from "@/modules/ui/components/file-input";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { Switch } from "@/modules/ui/components/switch";
|
||||
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||
import {
|
||||
determineImageUploaderVisibility,
|
||||
@@ -315,70 +313,6 @@ export const ElementFormInput = ({
|
||||
return false;
|
||||
};
|
||||
|
||||
const getIsRequiredToggleDisabled = (): boolean => {
|
||||
if (!currentElement) return false;
|
||||
|
||||
// CTA elements should always have the required toggle disabled
|
||||
if (currentElement.type === TSurveyElementTypeEnum.CTA) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentElement.type === TSurveyElementTypeEnum.Address) {
|
||||
const allFieldsAreOptional = [
|
||||
currentElement.addressLine1,
|
||||
currentElement.addressLine2,
|
||||
currentElement.city,
|
||||
currentElement.state,
|
||||
currentElement.zip,
|
||||
currentElement.country,
|
||||
]
|
||||
.filter((field) => field.show)
|
||||
.every((field) => !field.required);
|
||||
|
||||
if (allFieldsAreOptional) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return [
|
||||
currentElement.addressLine1,
|
||||
currentElement.addressLine2,
|
||||
currentElement.city,
|
||||
currentElement.state,
|
||||
currentElement.zip,
|
||||
currentElement.country,
|
||||
]
|
||||
.filter((field) => field.show)
|
||||
.some((condition) => condition.required === true);
|
||||
}
|
||||
|
||||
if (currentElement.type === TSurveyElementTypeEnum.ContactInfo) {
|
||||
const allFieldsAreOptional = [
|
||||
currentElement.firstName,
|
||||
currentElement.lastName,
|
||||
currentElement.email,
|
||||
currentElement.phone,
|
||||
currentElement.company,
|
||||
]
|
||||
.filter((field) => field.show)
|
||||
.every((field) => !field.required);
|
||||
|
||||
if (allFieldsAreOptional) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return [
|
||||
currentElement.firstName,
|
||||
currentElement.lastName,
|
||||
currentElement.email,
|
||||
currentElement.phone,
|
||||
currentElement.company,
|
||||
]
|
||||
.filter((field) => field.show)
|
||||
.some((condition) => condition.required === true);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const useRichTextEditor = id === "headline" || id === "subheader" || id === "html";
|
||||
|
||||
@@ -393,21 +327,6 @@ export const ElementFormInput = ({
|
||||
{label && (
|
||||
<div className="mb-2 mt-3 flex items-center justify-between">
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
{id === "headline" && currentElement && updateElement && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="required-toggle" className="text-sm">
|
||||
{t("environments.surveys.edit.required")}
|
||||
</Label>
|
||||
<Switch
|
||||
id="required-toggle"
|
||||
checked={currentElement.required}
|
||||
disabled={getIsRequiredToggleDisabled()}
|
||||
onCheckedChange={(checked) => {
|
||||
updateElement(elementIdx, { required: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-4" ref={animationParent}>
|
||||
@@ -523,21 +442,6 @@ export const ElementFormInput = ({
|
||||
{label && (
|
||||
<div className="mb-2 mt-3 flex items-center justify-between">
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
{id === "headline" && currentElement && updateElement && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="required-toggle" className="text-sm">
|
||||
{t("environments.surveys.edit.required")}
|
||||
</Label>
|
||||
<Switch
|
||||
id="required-toggle"
|
||||
checked={currentElement.required}
|
||||
disabled={getIsRequiredToggleDisabled()}
|
||||
onCheckedChange={(checked) => {
|
||||
updateElement(elementIdx, { required: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<MultiLangWrapper
|
||||
|
||||
@@ -4,11 +4,13 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { type JSX, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurveyAddressElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurveyAddressElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TValidationRulesForAddress } from "@formbricks/types/surveys/validation-rules";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { ElementToggleTable } from "@/modules/ui/components/element-toggle-table";
|
||||
|
||||
@@ -159,6 +161,16 @@ export const AddressElementForm = ({
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ValidationRulesEditor
|
||||
elementType={TSurveyElementTypeEnum.Address}
|
||||
validationRules={element.validationRules ?? []}
|
||||
onUpdateRules={(rules: TValidationRulesForAddress) => {
|
||||
updateElement(elementIdx, {
|
||||
validationRules: rules,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { type JSX, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurveyCalElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurveyCalElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TValidationRulesForCal } from "@formbricks/types/surveys/validation-rules";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
|
||||
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
@@ -143,6 +145,16 @@ export const CalElementForm = ({
|
||||
</AdvancedOptionToggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ValidationRulesEditor
|
||||
elementType={TSurveyElementTypeEnum.Cal}
|
||||
validationRules={element.validationRules ?? []}
|
||||
onUpdateRules={(rules: TValidationRulesForCal) => {
|
||||
updateElement(elementIdx, {
|
||||
validationRules: rules,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,11 +4,13 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { type JSX } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurveyConsentElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurveyConsentElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TValidationRulesForConsent } from "@formbricks/types/surveys/validation-rules";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
|
||||
interface ConsentElementFormProps {
|
||||
@@ -102,6 +104,16 @@ export const ConsentElementForm = ({
|
||||
placeholder="I agree to the terms and conditions"
|
||||
value={element.label}
|
||||
/>
|
||||
|
||||
<ValidationRulesEditor
|
||||
elementType={TSurveyElementTypeEnum.Consent}
|
||||
validationRules={element.validationRules ?? []}
|
||||
onUpdateRules={(rules: TValidationRulesForConsent) => {
|
||||
updateElement(elementIdx, {
|
||||
validationRules: rules,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,11 +4,13 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { type JSX, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurveyContactInfoElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurveyContactInfoElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TValidationRulesForContactInfo } from "@formbricks/types/surveys/validation-rules";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { ElementToggleTable } from "@/modules/ui/components/element-toggle-table";
|
||||
|
||||
@@ -156,6 +158,16 @@ export const ContactInfoElementForm = ({
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ValidationRulesEditor
|
||||
elementType={TSurveyElementTypeEnum.ContactInfo}
|
||||
validationRules={element.validationRules ?? []}
|
||||
onUpdateRules={(rules: TValidationRulesForContactInfo) => {
|
||||
updateElement(elementIdx, {
|
||||
validationRules: rules,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,11 +4,13 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { type JSX } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurveyDateElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurveyDateElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TValidationRulesForDate } from "@formbricks/types/surveys/validation-rules";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { OptionsSwitch } from "@/modules/ui/components/options-switch";
|
||||
@@ -126,6 +128,16 @@ export const DateElementForm = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ValidationRulesEditor
|
||||
elementType={TSurveyElementTypeEnum.Date}
|
||||
validationRules={element.validationRules ?? []}
|
||||
onUpdateRules={(rules: TValidationRulesForDate) => {
|
||||
updateElement(elementIdx, {
|
||||
validationRules: rules,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,11 +8,13 @@ import { type JSX, useMemo, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TAllowedFileExtension, ZAllowedFileExtension } from "@formbricks/types/storage";
|
||||
import { TSurveyFileUploadElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurveyElementTypeEnum, TSurveyFileUploadElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TValidationRulesForFileUpload } from "@formbricks/types/surveys/validation-rules";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
|
||||
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
@@ -229,7 +231,7 @@ export const FileUploadElementForm = ({
|
||||
|
||||
updateElement(elementIdx, { maxSizeInMB: parseInt(e.target.value, 10) });
|
||||
}}
|
||||
className="ml-2 mr-2 inline w-20 bg-white text-center text-sm"
|
||||
className="mr-2 ml-2 inline w-20 bg-white text-center text-sm"
|
||||
/>
|
||||
MB
|
||||
</p>
|
||||
@@ -290,6 +292,16 @@ export const FileUploadElementForm = ({
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
</div>
|
||||
|
||||
<ValidationRulesEditor
|
||||
elementType={TSurveyElementTypeEnum.FileUpload}
|
||||
validationRules={element.validationRules ?? []}
|
||||
onUpdateRules={(rules: TValidationRulesForFileUpload) => {
|
||||
updateElement(elementIdx, {
|
||||
validationRules: rules,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,12 +9,14 @@ import { type JSX, useCallback } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyMatrixElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurveyElementTypeEnum, TSurveyMatrixElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TValidationRulesForMatrix } from "@formbricks/types/surveys/validation-rules";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||
import { MatrixSortableItem } from "@/modules/survey/editor/components/matrix-sortable-item";
|
||||
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
|
||||
import { findOptionUsedInLogic } from "@/modules/survey/editor/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
@@ -347,6 +349,16 @@ export const MatrixElementForm = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ValidationRulesEditor
|
||||
elementType={TSurveyElementTypeEnum.Matrix}
|
||||
validationRules={element.validationRules ?? []}
|
||||
onUpdateRules={(rules: TValidationRulesForMatrix) => {
|
||||
updateElement(elementIdx, {
|
||||
validationRules: rules,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,11 +12,16 @@ import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
|
||||
import { TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyElementTypeEnum, TSurveyMultipleChoiceElement } from "@formbricks/types/surveys/elements";
|
||||
import { TShuffleOption, TSurvey } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
TValidationRulesForMultipleChoiceMulti,
|
||||
TValidationRulesForMultipleChoiceSingle,
|
||||
} from "@formbricks/types/surveys/validation-rules";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||
import { BulkEditOptionsModal } from "@/modules/survey/editor/components/bulk-edit-options-modal";
|
||||
import { ElementOptionChoice } from "@/modules/survey/editor/components/element-option-choice";
|
||||
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
|
||||
import { findOptionUsedInLogic } from "@/modules/survey/editor/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
@@ -398,6 +403,28 @@ export const MultipleChoiceElementForm = ({
|
||||
surveyLanguageCodes={surveyLanguageCodes}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
{element.type === TSurveyElementTypeEnum.MultipleChoiceMulti ? (
|
||||
<ValidationRulesEditor
|
||||
elementType={TSurveyElementTypeEnum.MultipleChoiceMulti}
|
||||
validationRules={element.validationRules ?? []}
|
||||
onUpdateRules={(rules: TValidationRulesForMultipleChoiceMulti) => {
|
||||
updateElement(elementIdx, {
|
||||
validationRules: rules,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ValidationRulesEditor
|
||||
elementType={TSurveyElementTypeEnum.MultipleChoiceSingle}
|
||||
validationRules={element.validationRules ?? []}
|
||||
onUpdateRules={(rules: TValidationRulesForMultipleChoiceSingle) => {
|
||||
updateElement(elementIdx, {
|
||||
validationRules: rules,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,11 +4,13 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { type JSX } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurveyNPSElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurveyElementTypeEnum, TSurveyNPSElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TValidationRulesForNPS } from "@formbricks/types/surveys/validation-rules";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
|
||||
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
|
||||
@@ -140,6 +142,16 @@ export const NPSElementForm = ({
|
||||
childBorder
|
||||
customContainerClass="p-0 mt-4"
|
||||
/>
|
||||
|
||||
<ValidationRulesEditor
|
||||
elementType={TSurveyElementTypeEnum.NPS}
|
||||
validationRules={element.validationRules ?? []}
|
||||
onUpdateRules={(rules: TValidationRulesForNPS) => {
|
||||
updateElement(elementIdx, {
|
||||
validationRules: rules,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { HashIcon, LinkIcon, MailIcon, MessageSquareTextIcon, PhoneIcon, PlusIcon } from "lucide-react";
|
||||
import { JSX, useEffect, useState } from "react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { JSX } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurveyOpenTextElement, TSurveyOpenTextElementInputType } from "@formbricks/types/surveys/elements";
|
||||
import {
|
||||
TSurveyElementTypeEnum,
|
||||
TSurveyOpenTextElement,
|
||||
TSurveyOpenTextElementInputType,
|
||||
} from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TValidationRulesForOpenText } from "@formbricks/types/surveys/validation-rules";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
|
||||
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { OptionsSwitch } from "@/modules/ui/components/options-switch";
|
||||
|
||||
interface OpenElementFormProps {
|
||||
localSurvey: TSurvey;
|
||||
@@ -42,43 +45,10 @@ export const OpenElementForm = ({
|
||||
isExternalUrlsAllowed,
|
||||
}: OpenElementFormProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const elementTypes = [
|
||||
{ value: "text", label: t("common.text"), icon: <MessageSquareTextIcon className="h-4 w-4" /> },
|
||||
{ value: "email", label: t("common.email"), icon: <MailIcon className="h-4 w-4" /> },
|
||||
{ value: "url", label: t("common.url"), icon: <LinkIcon className="h-4 w-4" /> },
|
||||
{ value: "number", label: t("common.number"), icon: <HashIcon className="h-4 w-4" /> },
|
||||
{ value: "phone", label: t("common.phone"), icon: <PhoneIcon className="h-4 w-4" /> },
|
||||
];
|
||||
const defaultPlaceholder = getPlaceholderByInputType(element.inputType ?? "text");
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
|
||||
|
||||
const [showCharLimits, setShowCharLimits] = useState(element.inputType === "text");
|
||||
|
||||
const handleInputChange = (inputType: TSurveyOpenTextElementInputType) => {
|
||||
const updatedAttributes = {
|
||||
inputType: inputType,
|
||||
placeholder: createI18nString(getPlaceholderByInputType(inputType), surveyLanguageCodes),
|
||||
longAnswer: inputType === "text" ? element.longAnswer : false,
|
||||
charLimit: {
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
},
|
||||
};
|
||||
setIsCharLimitEnabled(false);
|
||||
setShowCharLimits(inputType === "text");
|
||||
updateElement(elementIdx, updatedAttributes);
|
||||
};
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
const [isCharLimitEnabled, setIsCharLimitEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (element?.charLimit?.min !== undefined || element?.charLimit?.max !== undefined) {
|
||||
setIsCharLimitEnabled(true);
|
||||
} else {
|
||||
setIsCharLimitEnabled(false);
|
||||
}
|
||||
}, [element?.charLimit?.max, element?.charLimit?.min]);
|
||||
|
||||
return (
|
||||
<form>
|
||||
@@ -156,80 +126,7 @@ export const OpenElementForm = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Add a dropdown to select the element type */}
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="elementType">{t("common.input_type")}</Label>
|
||||
<div className="mt-2 flex items-center">
|
||||
<OptionsSwitch
|
||||
options={elementTypes}
|
||||
currentOption={element.inputType}
|
||||
handleOptionChange={handleInputChange} // Use the merged function
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 space-y-6">
|
||||
{showCharLimits && (
|
||||
<AdvancedOptionToggle
|
||||
isChecked={isCharLimitEnabled}
|
||||
onToggle={(checked: boolean) => {
|
||||
setIsCharLimitEnabled(checked);
|
||||
updateElement(elementIdx, {
|
||||
charLimit: {
|
||||
enabled: checked,
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
},
|
||||
});
|
||||
}}
|
||||
htmlId={`charLimit-${element.id}`}
|
||||
description={t("environments.surveys.edit.character_limit_toggle_description")}
|
||||
childBorder
|
||||
title={t("environments.surveys.edit.character_limit_toggle_title")}
|
||||
customContainerClass="p-0">
|
||||
<div className="flex gap-4 p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="minLength">{t("common.minimum")}</Label>
|
||||
<Input
|
||||
id="minLength"
|
||||
name="minLength"
|
||||
type="number"
|
||||
min={0}
|
||||
value={element?.charLimit?.min || ""}
|
||||
aria-label={t("common.minimum")}
|
||||
className="bg-white"
|
||||
onChange={(e) =>
|
||||
updateElement(elementIdx, {
|
||||
charLimit: {
|
||||
...element?.charLimit,
|
||||
min: e.target.value ? parseInt(e.target.value) : undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="maxLength">{t("common.maximum")}</Label>
|
||||
<Input
|
||||
id="maxLength"
|
||||
name="maxLength"
|
||||
type="number"
|
||||
min={0}
|
||||
aria-label={t("common.maximum")}
|
||||
value={element?.charLimit?.max || ""}
|
||||
className="bg-white"
|
||||
onChange={(e) =>
|
||||
updateElement(elementIdx, {
|
||||
charLimit: {
|
||||
...element?.charLimit,
|
||||
max: e.target.value ? parseInt(e.target.value) : undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
)}
|
||||
<div className="mt-4">
|
||||
<AdvancedOptionToggle
|
||||
isChecked={element.longAnswer !== false}
|
||||
@@ -245,6 +142,16 @@ export const OpenElementForm = ({
|
||||
customContainerClass="p-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ValidationRulesEditor
|
||||
elementType={TSurveyElementTypeEnum.OpenText}
|
||||
validationRules={element.validationRules ?? []}
|
||||
onUpdateRules={(rules: TValidationRulesForOpenText) => {
|
||||
updateElement(elementIdx, {
|
||||
validationRules: rules,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -5,12 +5,14 @@ import { createId } from "@paralleldrive/cuid2";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { type JSX } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurveyPictureSelectionElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurveyElementTypeEnum, TSurveyPictureSelectionElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TValidationRulesForPictureSelection } from "@formbricks/types/surveys/validation-rules";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { FileInput } from "@/modules/ui/components/file-input";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
@@ -170,6 +172,16 @@ export const PictureSelectionForm = ({
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<ValidationRulesEditor
|
||||
elementType={TSurveyElementTypeEnum.PictureSelection}
|
||||
validationRules={element.validationRules ?? []}
|
||||
onUpdateRules={(rules: TValidationRulesForPictureSelection) => {
|
||||
updateElement(elementIdx, {
|
||||
validationRules: rules,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,12 +8,14 @@ import { PlusIcon } from "lucide-react";
|
||||
import { type JSX, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurveyRankingElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurveyElementTypeEnum, TSurveyRankingElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TValidationRulesForRanking } from "@formbricks/types/surveys/validation-rules";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||
import { ElementOptionChoice } from "@/modules/survey/editor/components/element-option-choice";
|
||||
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { ShuffleOptionSelect } from "@/modules/ui/components/shuffle-option-select";
|
||||
@@ -246,6 +248,16 @@ export const RankingElementForm = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ValidationRulesEditor
|
||||
elementType={TSurveyElementTypeEnum.Ranking}
|
||||
validationRules={element.validationRules ?? []}
|
||||
onUpdateRules={(rules: TValidationRulesForRanking) => {
|
||||
updateElement(elementIdx, {
|
||||
validationRules: rules,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { HashIcon, PlusIcon, SmileIcon, StarIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurveyRatingElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurveyElementTypeEnum, TSurveyRatingElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TValidationRulesForRating } from "@formbricks/types/surveys/validation-rules";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||
import { Dropdown } from "@/modules/survey/editor/components/rating-type-dropdown";
|
||||
import { ValidationRulesEditor } from "@/modules/survey/editor/components/validation-rules-editor";
|
||||
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
@@ -189,6 +191,16 @@ export const RatingElementForm = ({
|
||||
customContainerClass="p-0 mt-4"
|
||||
/>
|
||||
)}
|
||||
|
||||
<ValidationRulesEditor
|
||||
elementType={TSurveyElementTypeEnum.Rating}
|
||||
validationRules={element.validationRules ?? []}
|
||||
onUpdateRules={(rules: TValidationRulesForRating) => {
|
||||
updateElement(elementIdx, {
|
||||
validationRules: rules,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { GripVerticalIcon } from "lucide-react";
|
||||
|
||||
interface ValidationRuleItemProps {
|
||||
id: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ValidationRuleItem = ({ id, children }: ValidationRuleItemProps) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
zIndex: isDragging ? 10 : 1,
|
||||
position: isDragging ? "relative" : "static",
|
||||
} as React.CSSProperties;
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} className="flex w-full items-center gap-2">
|
||||
<div {...attributes} {...listeners} className="cursor-move text-slate-400 hover:text-slate-600">
|
||||
<GripVerticalIcon className="h-4 w-4" />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,261 @@
|
||||
"use client";
|
||||
|
||||
import { DndContext, DragEndEvent, PointerSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
|
||||
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 { TSurveyElementTypeEnum } 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";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/modules/ui/components/select";
|
||||
import { cn } from "@/modules/ui/lib/utils";
|
||||
import { RULE_TYPE_CONFIG } from "../lib/validation-rules-config";
|
||||
import { createRuleParams, getAvailableRuleTypes, getRuleValue } from "../lib/validation-rules-utils";
|
||||
import { ValidationRuleItem } from "./validation-rule-item";
|
||||
|
||||
interface ValidationRulesEditorProps {
|
||||
elementType: TSurveyElementTypeEnum;
|
||||
validationRules: TValidationRule[];
|
||||
onUpdateRules: (rules: TValidationRule[]) => void;
|
||||
}
|
||||
|
||||
export const ValidationRulesEditor = ({
|
||||
elementType,
|
||||
validationRules,
|
||||
onUpdateRules,
|
||||
}: ValidationRulesEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const ruleLabels: Record<string, string> = {
|
||||
required: t("environments.surveys.edit.validation.required"),
|
||||
min_length: t("environments.surveys.edit.validation.min_length"),
|
||||
max_length: t("environments.surveys.edit.validation.max_length"),
|
||||
pattern: t("environments.surveys.edit.validation.pattern"),
|
||||
email: t("environments.surveys.edit.validation.email"),
|
||||
url: t("environments.surveys.edit.validation.url"),
|
||||
phone: t("environments.surveys.edit.validation.phone"),
|
||||
min_value: t("environments.surveys.edit.validation.min_value"),
|
||||
max_value: t("environments.surveys.edit.validation.max_value"),
|
||||
min_selections: t("environments.surveys.edit.validation.min_selections"),
|
||||
max_selections: t("environments.surveys.edit.validation.max_selections"),
|
||||
characters: t("environments.surveys.edit.validation.characters"),
|
||||
options_selected: t("environments.surveys.edit.validation.options_selected"),
|
||||
};
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 5,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const isEnabled = validationRules.length > 0;
|
||||
|
||||
const handleEnable = () => {
|
||||
const availableRules = getAvailableRuleTypes(elementType, []);
|
||||
if (availableRules.length > 0) {
|
||||
const defaultRuleType = availableRules[0];
|
||||
const config = RULE_TYPE_CONFIG[defaultRuleType];
|
||||
let defaultValue: number | string | undefined = undefined;
|
||||
if (config.needsValue && config.valueType === "text") {
|
||||
defaultValue = "";
|
||||
}
|
||||
const newRule: TValidationRule = {
|
||||
id: uuidv7(),
|
||||
type: defaultRuleType,
|
||||
params: createRuleParams(defaultRuleType, defaultValue),
|
||||
} as TValidationRule;
|
||||
onUpdateRules([newRule]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisable = () => {
|
||||
onUpdateRules([]);
|
||||
};
|
||||
|
||||
const handleAddRule = (insertAfterIndex: number) => {
|
||||
const availableRules = getAvailableRuleTypes(elementType, validationRules);
|
||||
if (availableRules.length === 0) return;
|
||||
|
||||
const newRuleType = availableRules[0];
|
||||
const config = RULE_TYPE_CONFIG[newRuleType];
|
||||
let defaultValue: number | string | undefined = undefined;
|
||||
if (config.needsValue && config.valueType === "text") {
|
||||
defaultValue = "";
|
||||
}
|
||||
const newRule: TValidationRule = {
|
||||
id: uuidv7(),
|
||||
type: newRuleType,
|
||||
params: createRuleParams(newRuleType, defaultValue),
|
||||
} as TValidationRule;
|
||||
const newRules = [...validationRules];
|
||||
newRules.splice(insertAfterIndex + 1, 0, newRule);
|
||||
onUpdateRules(newRules);
|
||||
};
|
||||
|
||||
const handleDeleteRule = (ruleId: string) => {
|
||||
const updated = validationRules.filter((r) => r.id !== ruleId);
|
||||
onUpdateRules(updated);
|
||||
};
|
||||
|
||||
const handleRuleTypeChange = (ruleId: string, newType: TValidationRuleType) => {
|
||||
const updated = validationRules.map((rule) => {
|
||||
if (rule.id !== ruleId) return rule;
|
||||
return {
|
||||
...rule,
|
||||
type: newType,
|
||||
params: createRuleParams(newType),
|
||||
} as TValidationRule;
|
||||
});
|
||||
onUpdateRules(updated);
|
||||
};
|
||||
|
||||
const handleRuleValueChange = (ruleId: string, value: string) => {
|
||||
const updated = validationRules.map((rule) => {
|
||||
if (rule.id !== ruleId) return rule;
|
||||
const ruleType = rule.type;
|
||||
const config = RULE_TYPE_CONFIG[ruleType];
|
||||
const parsedValue = config.valueType === "number" ? Number(value) || 0 : value;
|
||||
return {
|
||||
...rule,
|
||||
params: createRuleParams(ruleType, parsedValue),
|
||||
} as TValidationRule;
|
||||
});
|
||||
onUpdateRules(updated);
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (!over || active.id === over.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldIndex = validationRules.findIndex((rule) => rule.id === active.id);
|
||||
const newIndex = validationRules.findIndex((rule) => rule.id === over.id);
|
||||
|
||||
if (oldIndex !== -1 && newIndex !== -1) {
|
||||
const newRules = [...validationRules];
|
||||
const [movedRule] = newRules.splice(oldIndex, 1);
|
||||
newRules.splice(newIndex, 0, movedRule);
|
||||
onUpdateRules(newRules);
|
||||
}
|
||||
};
|
||||
|
||||
const availableRulesForAdd = getAvailableRuleTypes(elementType, validationRules);
|
||||
const canAddMore = availableRulesForAdd.length > 0;
|
||||
|
||||
return (
|
||||
<AdvancedOptionToggle
|
||||
isChecked={isEnabled}
|
||||
onToggle={(checked) => (checked ? handleEnable() : handleDisable())}
|
||||
htmlId="validation-rules-toggle"
|
||||
title={t("environments.surveys.edit.validation_rules")}
|
||||
description={t("environments.surveys.edit.validation_rules_description")}
|
||||
customContainerClass="p-0 mt-4"
|
||||
childrenContainerClass="flex-col p-3 gap-2">
|
||||
<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">
|
||||
{validationRules.map((rule, index) => {
|
||||
const ruleType = rule.type;
|
||||
const config = RULE_TYPE_CONFIG[ruleType];
|
||||
const currentValue = getRuleValue(rule);
|
||||
|
||||
// Get available types for this rule (current type + unused types, no duplicates)
|
||||
const otherAvailableTypes = getAvailableRuleTypes(
|
||||
elementType,
|
||||
validationRules.filter((r) => r.id !== rule.id)
|
||||
).filter((t) => t !== ruleType);
|
||||
const availableTypesForSelect = [ruleType, ...otherAvailableTypes];
|
||||
|
||||
return (
|
||||
<ValidationRuleItem key={rule.id} id={rule.id}>
|
||||
{/* Rule Type Selector */}
|
||||
<Select
|
||||
value={ruleType}
|
||||
onValueChange={(value) => handleRuleTypeChange(rule.id, value as TValidationRuleType)}>
|
||||
<SelectTrigger className={cn("bg-white", config.needsValue ? "w-[200px]" : "flex-1")}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableTypesForSelect.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{ruleLabels[RULE_TYPE_CONFIG[type].labelKey]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Value Input (if needed) */}
|
||||
{config.needsValue && (
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Input
|
||||
type={config.valueType === "number" ? "number" : "text"}
|
||||
value={currentValue ?? ""}
|
||||
onChange={(e) => handleRuleValueChange(rule.id, e.target.value)}
|
||||
placeholder={config.valuePlaceholder}
|
||||
className="h-9 min-w-[80px] bg-white"
|
||||
min={config.valueType === "number" ? 0 : ""}
|
||||
/>
|
||||
|
||||
{/* Unit selector (if applicable) */}
|
||||
{config.unitOptions && config.unitOptions.length > 0 && (
|
||||
<Select value={config.unitOptions[0].value}>
|
||||
<SelectTrigger
|
||||
className="flex-1 bg-white"
|
||||
disabled={config.unitOptions.length === 1}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{config.unitOptions.map((unit) => (
|
||||
<SelectItem key={unit.value} value={unit.value}>
|
||||
{ruleLabels[unit.labelKey]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
type="button"
|
||||
onClick={() => handleDeleteRule(rule.id)}
|
||||
className="shrink-0 bg-white">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{/* Add button */}
|
||||
{canAddMore && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
type="button"
|
||||
onClick={() => handleAddRule(index)}
|
||||
className="shrink-0 bg-white">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</ValidationRuleItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</AdvancedOptionToggle>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,190 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { TValidationRuleType } from "@formbricks/types/surveys/validation-rules";
|
||||
import { RULE_TYPE_CONFIG } from "./validation-rules-config";
|
||||
|
||||
describe("RULE_TYPE_CONFIG", () => {
|
||||
test("should have config for all validation rule types", () => {
|
||||
const allRuleTypes: TValidationRuleType[] = [
|
||||
"required",
|
||||
"minLength",
|
||||
"maxLength",
|
||||
"pattern",
|
||||
"email",
|
||||
"url",
|
||||
"phone",
|
||||
"minValue",
|
||||
"maxValue",
|
||||
"minSelections",
|
||||
"maxSelections",
|
||||
];
|
||||
|
||||
allRuleTypes.forEach((ruleType) => {
|
||||
expect(RULE_TYPE_CONFIG[ruleType]).toBeDefined();
|
||||
expect(RULE_TYPE_CONFIG[ruleType].labelKey).toBeDefined();
|
||||
expect(typeof RULE_TYPE_CONFIG[ruleType].labelKey).toBe("string");
|
||||
expect(typeof RULE_TYPE_CONFIG[ruleType].needsValue).toBe("boolean");
|
||||
});
|
||||
});
|
||||
|
||||
describe("required rule", () => {
|
||||
test("should have correct config", () => {
|
||||
const config = RULE_TYPE_CONFIG.required;
|
||||
expect(config.labelKey).toBe("required");
|
||||
expect(config.needsValue).toBe(false);
|
||||
expect(config.valueType).toBeUndefined();
|
||||
expect(config.valuePlaceholder).toBeUndefined();
|
||||
expect(config.unitOptions).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("minLength rule", () => {
|
||||
test("should have correct config", () => {
|
||||
const config = RULE_TYPE_CONFIG.minLength;
|
||||
expect(config.labelKey).toBe("min_length");
|
||||
expect(config.needsValue).toBe(true);
|
||||
expect(config.valueType).toBe("number");
|
||||
expect(config.valuePlaceholder).toBe("100");
|
||||
expect(config.unitOptions).toEqual([{ value: "characters", labelKey: "characters" }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("maxLength rule", () => {
|
||||
test("should have correct config", () => {
|
||||
const config = RULE_TYPE_CONFIG.maxLength;
|
||||
expect(config.labelKey).toBe("max_length");
|
||||
expect(config.needsValue).toBe(true);
|
||||
expect(config.valueType).toBe("number");
|
||||
expect(config.valuePlaceholder).toBe("500");
|
||||
expect(config.unitOptions).toEqual([{ value: "characters", labelKey: "characters" }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pattern rule", () => {
|
||||
test("should have correct config", () => {
|
||||
const config = RULE_TYPE_CONFIG.pattern;
|
||||
expect(config.labelKey).toBe("pattern");
|
||||
expect(config.needsValue).toBe(true);
|
||||
expect(config.valueType).toBe("text");
|
||||
expect(config.valuePlaceholder).toBe("^[A-Z].*");
|
||||
expect(config.unitOptions).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("email rule", () => {
|
||||
test("should have correct config", () => {
|
||||
const config = RULE_TYPE_CONFIG.email;
|
||||
expect(config.labelKey).toBe("email");
|
||||
expect(config.needsValue).toBe(false);
|
||||
expect(config.valueType).toBeUndefined();
|
||||
expect(config.valuePlaceholder).toBeUndefined();
|
||||
expect(config.unitOptions).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("url rule", () => {
|
||||
test("should have correct config", () => {
|
||||
const config = RULE_TYPE_CONFIG.url;
|
||||
expect(config.labelKey).toBe("url");
|
||||
expect(config.needsValue).toBe(false);
|
||||
expect(config.valueType).toBeUndefined();
|
||||
expect(config.valuePlaceholder).toBeUndefined();
|
||||
expect(config.unitOptions).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("phone rule", () => {
|
||||
test("should have correct config", () => {
|
||||
const config = RULE_TYPE_CONFIG.phone;
|
||||
expect(config.labelKey).toBe("phone");
|
||||
expect(config.needsValue).toBe(false);
|
||||
expect(config.valueType).toBeUndefined();
|
||||
expect(config.valuePlaceholder).toBeUndefined();
|
||||
expect(config.unitOptions).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("minValue rule", () => {
|
||||
test("should have correct config", () => {
|
||||
const config = RULE_TYPE_CONFIG.minValue;
|
||||
expect(config.labelKey).toBe("min_value");
|
||||
expect(config.needsValue).toBe(true);
|
||||
expect(config.valueType).toBe("number");
|
||||
expect(config.valuePlaceholder).toBe("0");
|
||||
expect(config.unitOptions).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("maxValue rule", () => {
|
||||
test("should have correct config", () => {
|
||||
const config = RULE_TYPE_CONFIG.maxValue;
|
||||
expect(config.labelKey).toBe("max_value");
|
||||
expect(config.needsValue).toBe(true);
|
||||
expect(config.valueType).toBe("number");
|
||||
expect(config.valuePlaceholder).toBe("100");
|
||||
expect(config.unitOptions).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("minSelections rule", () => {
|
||||
test("should have correct config", () => {
|
||||
const config = RULE_TYPE_CONFIG.minSelections;
|
||||
expect(config.labelKey).toBe("min_selections");
|
||||
expect(config.needsValue).toBe(true);
|
||||
expect(config.valueType).toBe("number");
|
||||
expect(config.valuePlaceholder).toBe("1");
|
||||
expect(config.unitOptions).toEqual([{ value: "options", labelKey: "options_selected" }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("maxSelections rule", () => {
|
||||
test("should have correct config", () => {
|
||||
const config = RULE_TYPE_CONFIG.maxSelections;
|
||||
expect(config.labelKey).toBe("max_selections");
|
||||
expect(config.needsValue).toBe(true);
|
||||
expect(config.valueType).toBe("number");
|
||||
expect(config.valuePlaceholder).toBe("3");
|
||||
expect(config.unitOptions).toEqual([{ value: "options", labelKey: "options_selected" }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("valueType validation", () => {
|
||||
test("should have valueType 'number' for numeric rules", () => {
|
||||
expect(RULE_TYPE_CONFIG.minLength.valueType).toBe("number");
|
||||
expect(RULE_TYPE_CONFIG.maxLength.valueType).toBe("number");
|
||||
expect(RULE_TYPE_CONFIG.minValue.valueType).toBe("number");
|
||||
expect(RULE_TYPE_CONFIG.maxValue.valueType).toBe("number");
|
||||
expect(RULE_TYPE_CONFIG.minSelections.valueType).toBe("number");
|
||||
expect(RULE_TYPE_CONFIG.maxSelections.valueType).toBe("number");
|
||||
});
|
||||
|
||||
test("should have valueType 'text' for text rules", () => {
|
||||
expect(RULE_TYPE_CONFIG.pattern.valueType).toBe("text");
|
||||
});
|
||||
|
||||
test("should not have valueType for rules that don't need values", () => {
|
||||
expect(RULE_TYPE_CONFIG.required.valueType).toBeUndefined();
|
||||
expect(RULE_TYPE_CONFIG.email.valueType).toBeUndefined();
|
||||
expect(RULE_TYPE_CONFIG.url.valueType).toBeUndefined();
|
||||
expect(RULE_TYPE_CONFIG.phone.valueType).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("unitOptions validation", () => {
|
||||
test("should have unitOptions for length and selection rules", () => {
|
||||
expect(RULE_TYPE_CONFIG.minLength.unitOptions).toBeDefined();
|
||||
expect(RULE_TYPE_CONFIG.maxLength.unitOptions).toBeDefined();
|
||||
expect(RULE_TYPE_CONFIG.minSelections.unitOptions).toBeDefined();
|
||||
expect(RULE_TYPE_CONFIG.maxSelections.unitOptions).toBeDefined();
|
||||
});
|
||||
|
||||
test("should not have unitOptions for other rules", () => {
|
||||
expect(RULE_TYPE_CONFIG.required.unitOptions).toBeUndefined();
|
||||
expect(RULE_TYPE_CONFIG.pattern.unitOptions).toBeUndefined();
|
||||
expect(RULE_TYPE_CONFIG.email.unitOptions).toBeUndefined();
|
||||
expect(RULE_TYPE_CONFIG.url.unitOptions).toBeUndefined();
|
||||
expect(RULE_TYPE_CONFIG.phone.unitOptions).toBeUndefined();
|
||||
expect(RULE_TYPE_CONFIG.minValue.unitOptions).toBeUndefined();
|
||||
expect(RULE_TYPE_CONFIG.maxValue.unitOptions).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { TValidationRuleType } from "@formbricks/types/surveys/validation-rules";
|
||||
|
||||
// Rule type definitions with i18n keys
|
||||
export const RULE_TYPE_CONFIG: Record<
|
||||
TValidationRuleType,
|
||||
{
|
||||
labelKey: string;
|
||||
needsValue: boolean;
|
||||
valueType?: "number" | "text";
|
||||
valuePlaceholder?: string;
|
||||
unitOptions?: { value: string; labelKey: string }[];
|
||||
}
|
||||
> = {
|
||||
required: {
|
||||
labelKey: "required",
|
||||
needsValue: false,
|
||||
},
|
||||
minLength: {
|
||||
labelKey: "min_length",
|
||||
needsValue: true,
|
||||
valueType: "number",
|
||||
valuePlaceholder: "100",
|
||||
unitOptions: [{ value: "characters", labelKey: "characters" }],
|
||||
},
|
||||
maxLength: {
|
||||
labelKey: "max_length",
|
||||
needsValue: true,
|
||||
valueType: "number",
|
||||
valuePlaceholder: "500",
|
||||
unitOptions: [{ value: "characters", labelKey: "characters" }],
|
||||
},
|
||||
pattern: {
|
||||
labelKey: "pattern",
|
||||
needsValue: true,
|
||||
valueType: "text",
|
||||
valuePlaceholder: "^[A-Z].*",
|
||||
},
|
||||
email: {
|
||||
labelKey: "email",
|
||||
needsValue: false,
|
||||
},
|
||||
url: {
|
||||
labelKey: "url",
|
||||
needsValue: false,
|
||||
},
|
||||
phone: {
|
||||
labelKey: "phone",
|
||||
needsValue: false,
|
||||
},
|
||||
minValue: {
|
||||
labelKey: "min_value",
|
||||
needsValue: true,
|
||||
valueType: "number",
|
||||
valuePlaceholder: "0",
|
||||
},
|
||||
maxValue: {
|
||||
labelKey: "max_value",
|
||||
needsValue: true,
|
||||
valueType: "number",
|
||||
valuePlaceholder: "100",
|
||||
},
|
||||
minSelections: {
|
||||
labelKey: "min_selections",
|
||||
needsValue: true,
|
||||
valueType: "number",
|
||||
valuePlaceholder: "1",
|
||||
unitOptions: [{ value: "options", labelKey: "options_selected" }],
|
||||
},
|
||||
maxSelections: {
|
||||
labelKey: "max_selections",
|
||||
needsValue: true,
|
||||
valueType: "number",
|
||||
valuePlaceholder: "3",
|
||||
unitOptions: [{ value: "options", labelKey: "options_selected" }],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,486 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import { TValidationRule } from "@formbricks/types/surveys/validation-rules";
|
||||
import { createRuleParams, getAvailableRuleTypes, getRuleValue } from "./validation-rules-utils";
|
||||
|
||||
describe("getAvailableRuleTypes", () => {
|
||||
test("should return all applicable rules for openText element when no rules exist", () => {
|
||||
const elementType = TSurveyElementTypeEnum.OpenText;
|
||||
const existingRules: TValidationRule[] = [];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toContain("required");
|
||||
expect(available).toContain("minLength");
|
||||
expect(available).toContain("maxLength");
|
||||
expect(available).toContain("pattern");
|
||||
expect(available).toContain("email");
|
||||
expect(available).toContain("url");
|
||||
expect(available).toContain("phone");
|
||||
expect(available).toContain("minValue");
|
||||
expect(available).toContain("maxValue");
|
||||
});
|
||||
|
||||
test("should filter out already added rules", () => {
|
||||
const elementType = TSurveyElementTypeEnum.OpenText;
|
||||
const existingRules: TValidationRule[] = [
|
||||
{
|
||||
id: "rule1",
|
||||
type: "required",
|
||||
params: {},
|
||||
},
|
||||
{
|
||||
id: "rule2",
|
||||
type: "minLength",
|
||||
params: { min: 10 },
|
||||
},
|
||||
];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).not.toContain("required");
|
||||
expect(available).not.toContain("minLength");
|
||||
expect(available).toContain("maxLength");
|
||||
expect(available).toContain("pattern");
|
||||
});
|
||||
|
||||
test("should return only required rule for multipleChoiceSingle element", () => {
|
||||
const elementType = TSurveyElementTypeEnum.MultipleChoiceSingle;
|
||||
const existingRules: TValidationRule[] = [];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toEqual(["required"]);
|
||||
});
|
||||
|
||||
test("should return empty array for multipleChoiceSingle when required is already added", () => {
|
||||
const elementType = TSurveyElementTypeEnum.MultipleChoiceSingle;
|
||||
const existingRules: TValidationRule[] = [
|
||||
{
|
||||
id: "rule1",
|
||||
type: "required",
|
||||
params: {},
|
||||
},
|
||||
];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toEqual([]);
|
||||
});
|
||||
|
||||
test("should return required, minSelections, maxSelections for multipleChoiceMulti element", () => {
|
||||
const elementType = TSurveyElementTypeEnum.MultipleChoiceMulti;
|
||||
const existingRules: TValidationRule[] = [];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toContain("required");
|
||||
expect(available).toContain("minSelections");
|
||||
expect(available).toContain("maxSelections");
|
||||
expect(available.length).toBe(3);
|
||||
});
|
||||
|
||||
test("should return only required rule for rating element", () => {
|
||||
const elementType = TSurveyElementTypeEnum.Rating;
|
||||
const existingRules: TValidationRule[] = [];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toEqual(["required"]);
|
||||
});
|
||||
|
||||
test("should return only required rule for nps element", () => {
|
||||
const elementType = TSurveyElementTypeEnum.NPS;
|
||||
const existingRules: TValidationRule[] = [];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toEqual(["required"]);
|
||||
});
|
||||
|
||||
test("should return only required rule for date element", () => {
|
||||
const elementType = TSurveyElementTypeEnum.Date;
|
||||
const existingRules: TValidationRule[] = [];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toEqual(["required"]);
|
||||
});
|
||||
|
||||
test("should return only required rule for consent element", () => {
|
||||
const elementType = TSurveyElementTypeEnum.Consent;
|
||||
const existingRules: TValidationRule[] = [];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toEqual(["required"]);
|
||||
});
|
||||
|
||||
test("should return only required rule for matrix element", () => {
|
||||
const elementType = TSurveyElementTypeEnum.Matrix;
|
||||
const existingRules: TValidationRule[] = [];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toEqual(["required"]);
|
||||
});
|
||||
|
||||
test("should return only required rule for ranking element", () => {
|
||||
const elementType = TSurveyElementTypeEnum.Ranking;
|
||||
const existingRules: TValidationRule[] = [];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toEqual(["required"]);
|
||||
});
|
||||
|
||||
test("should return only required rule for fileUpload element", () => {
|
||||
const elementType = TSurveyElementTypeEnum.FileUpload;
|
||||
const existingRules: TValidationRule[] = [];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toEqual(["required"]);
|
||||
});
|
||||
|
||||
test("should return required, minSelections, maxSelections for pictureSelection element", () => {
|
||||
const elementType = TSurveyElementTypeEnum.PictureSelection;
|
||||
const existingRules: TValidationRule[] = [];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toContain("required");
|
||||
expect(available).toContain("minSelections");
|
||||
expect(available).toContain("maxSelections");
|
||||
expect(available.length).toBe(3);
|
||||
});
|
||||
|
||||
test("should return only required rule for address element", () => {
|
||||
const elementType = TSurveyElementTypeEnum.Address;
|
||||
const existingRules: TValidationRule[] = [];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toEqual(["required"]);
|
||||
});
|
||||
|
||||
test("should return only required rule for contactInfo element", () => {
|
||||
const elementType = TSurveyElementTypeEnum.ContactInfo;
|
||||
const existingRules: TValidationRule[] = [];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toEqual(["required"]);
|
||||
});
|
||||
|
||||
test("should return only required rule for cal element", () => {
|
||||
const elementType = TSurveyElementTypeEnum.Cal;
|
||||
const existingRules: TValidationRule[] = [];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toEqual(["required"]);
|
||||
});
|
||||
|
||||
test("should return empty array for cta element", () => {
|
||||
const elementType = TSurveyElementTypeEnum.CTA;
|
||||
const existingRules: TValidationRule[] = [];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toEqual([]);
|
||||
});
|
||||
|
||||
test("should handle unknown element type gracefully", () => {
|
||||
const elementType = "unknown" as TSurveyElementTypeEnum;
|
||||
const existingRules: TValidationRule[] = [];
|
||||
|
||||
const available = getAvailableRuleTypes(elementType, existingRules);
|
||||
|
||||
expect(available).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRuleValue", () => {
|
||||
test("should return min value for minLength rule", () => {
|
||||
const rule: TValidationRule = {
|
||||
id: "rule1",
|
||||
type: "minLength",
|
||||
params: { min: 10 },
|
||||
};
|
||||
|
||||
expect(getRuleValue(rule)).toBe(10);
|
||||
});
|
||||
|
||||
test("should return max value for maxLength rule", () => {
|
||||
const rule: TValidationRule = {
|
||||
id: "rule2",
|
||||
type: "maxLength",
|
||||
params: { max: 100 },
|
||||
};
|
||||
|
||||
expect(getRuleValue(rule)).toBe(100);
|
||||
});
|
||||
|
||||
test("should return pattern string for pattern rule", () => {
|
||||
const rule: TValidationRule = {
|
||||
id: "rule3",
|
||||
type: "pattern",
|
||||
params: { pattern: "^[A-Z].*" },
|
||||
};
|
||||
|
||||
expect(getRuleValue(rule)).toBe("^[A-Z].*");
|
||||
});
|
||||
|
||||
test("should return pattern string with flags for pattern rule", () => {
|
||||
const rule: TValidationRule = {
|
||||
id: "rule3",
|
||||
type: "pattern",
|
||||
params: { pattern: "^[A-Z].*", flags: "i" },
|
||||
};
|
||||
|
||||
expect(getRuleValue(rule)).toBe("^[A-Z].*");
|
||||
});
|
||||
|
||||
test("should return min value for minValue rule", () => {
|
||||
const rule: TValidationRule = {
|
||||
id: "rule4",
|
||||
type: "minValue",
|
||||
params: { min: 5 },
|
||||
};
|
||||
|
||||
expect(getRuleValue(rule)).toBe(5);
|
||||
});
|
||||
|
||||
test("should return max value for maxValue rule", () => {
|
||||
const rule: TValidationRule = {
|
||||
id: "rule5",
|
||||
type: "maxValue",
|
||||
params: { max: 50 },
|
||||
};
|
||||
|
||||
expect(getRuleValue(rule)).toBe(50);
|
||||
});
|
||||
|
||||
test("should return min value for minSelections rule", () => {
|
||||
const rule: TValidationRule = {
|
||||
id: "rule6",
|
||||
type: "minSelections",
|
||||
params: { min: 2 },
|
||||
};
|
||||
|
||||
expect(getRuleValue(rule)).toBe(2);
|
||||
});
|
||||
|
||||
test("should return max value for maxSelections rule", () => {
|
||||
const rule: TValidationRule = {
|
||||
id: "rule7",
|
||||
type: "maxSelections",
|
||||
params: { max: 5 },
|
||||
};
|
||||
|
||||
expect(getRuleValue(rule)).toBe(5);
|
||||
});
|
||||
|
||||
test("should return undefined for required rule", () => {
|
||||
const rule: TValidationRule = {
|
||||
id: "rule8",
|
||||
type: "required",
|
||||
params: {},
|
||||
};
|
||||
|
||||
expect(getRuleValue(rule)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should return undefined for email rule", () => {
|
||||
const rule: TValidationRule = {
|
||||
id: "rule9",
|
||||
type: "email",
|
||||
params: {},
|
||||
};
|
||||
|
||||
expect(getRuleValue(rule)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should return undefined for url rule", () => {
|
||||
const rule: TValidationRule = {
|
||||
id: "rule10",
|
||||
type: "url",
|
||||
params: {},
|
||||
};
|
||||
|
||||
expect(getRuleValue(rule)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should return undefined for phone rule", () => {
|
||||
const rule: TValidationRule = {
|
||||
id: "rule11",
|
||||
type: "phone",
|
||||
params: {},
|
||||
};
|
||||
|
||||
expect(getRuleValue(rule)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should return empty string for pattern rule with empty pattern", () => {
|
||||
const rule: TValidationRule = {
|
||||
id: "rule12",
|
||||
type: "pattern",
|
||||
params: { pattern: "" },
|
||||
};
|
||||
|
||||
expect(getRuleValue(rule)).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createRuleParams", () => {
|
||||
test("should create empty params for required rule", () => {
|
||||
const params = createRuleParams("required");
|
||||
expect(params).toEqual({});
|
||||
});
|
||||
|
||||
test("should create params for minLength rule with value", () => {
|
||||
const params = createRuleParams("minLength", 10);
|
||||
expect(params).toEqual({ min: 10 });
|
||||
});
|
||||
|
||||
test("should create params for minLength rule without value (defaults to 0)", () => {
|
||||
const params = createRuleParams("minLength");
|
||||
expect(params).toEqual({ min: 0 });
|
||||
});
|
||||
|
||||
test("should create params for maxLength rule with value", () => {
|
||||
const params = createRuleParams("maxLength", 100);
|
||||
expect(params).toEqual({ max: 100 });
|
||||
});
|
||||
|
||||
test("should create params for maxLength rule without value (defaults to 100)", () => {
|
||||
const params = createRuleParams("maxLength");
|
||||
expect(params).toEqual({ max: 100 });
|
||||
});
|
||||
|
||||
test("should create params for pattern rule with string value", () => {
|
||||
const params = createRuleParams("pattern", "^[A-Z].*");
|
||||
expect(params).toEqual({ pattern: "^[A-Z].*" });
|
||||
});
|
||||
|
||||
test("should create params for pattern rule without value (defaults to empty string)", () => {
|
||||
const params = createRuleParams("pattern");
|
||||
expect(params).toEqual({ pattern: "" });
|
||||
});
|
||||
|
||||
test("should create empty params for email rule", () => {
|
||||
const params = createRuleParams("email");
|
||||
expect(params).toEqual({});
|
||||
});
|
||||
|
||||
test("should create empty params for url rule", () => {
|
||||
const params = createRuleParams("url");
|
||||
expect(params).toEqual({});
|
||||
});
|
||||
|
||||
test("should create empty params for phone rule", () => {
|
||||
const params = createRuleParams("phone");
|
||||
expect(params).toEqual({});
|
||||
});
|
||||
|
||||
test("should create params for minValue rule with value", () => {
|
||||
const params = createRuleParams("minValue", 5);
|
||||
expect(params).toEqual({ min: 5 });
|
||||
});
|
||||
|
||||
test("should create params for minValue rule without value (defaults to 0)", () => {
|
||||
const params = createRuleParams("minValue");
|
||||
expect(params).toEqual({ min: 0 });
|
||||
});
|
||||
|
||||
test("should create params for maxValue rule with value", () => {
|
||||
const params = createRuleParams("maxValue", 50);
|
||||
expect(params).toEqual({ max: 50 });
|
||||
});
|
||||
|
||||
test("should create params for maxValue rule without value (defaults to 100)", () => {
|
||||
const params = createRuleParams("maxValue");
|
||||
expect(params).toEqual({ max: 100 });
|
||||
});
|
||||
|
||||
test("should create params for minSelections rule with value", () => {
|
||||
const params = createRuleParams("minSelections", 2);
|
||||
expect(params).toEqual({ min: 2 });
|
||||
});
|
||||
|
||||
test("should create params for minSelections rule without value (defaults to 1)", () => {
|
||||
const params = createRuleParams("minSelections");
|
||||
expect(params).toEqual({ min: 1 });
|
||||
});
|
||||
|
||||
test("should create params for maxSelections rule with value", () => {
|
||||
const params = createRuleParams("maxSelections", 5);
|
||||
expect(params).toEqual({ max: 5 });
|
||||
});
|
||||
|
||||
test("should create params for maxSelections rule without value (defaults to 3)", () => {
|
||||
const params = createRuleParams("maxSelections");
|
||||
expect(params).toEqual({ max: 3 });
|
||||
});
|
||||
|
||||
test("should convert string number to number for minLength", () => {
|
||||
const params = createRuleParams("minLength", "10");
|
||||
expect(params).toEqual({ min: 10 });
|
||||
});
|
||||
|
||||
test("should convert string number to number for maxLength", () => {
|
||||
const params = createRuleParams("maxLength", "100");
|
||||
expect(params).toEqual({ max: 100 });
|
||||
});
|
||||
|
||||
test("should convert string number to number for minValue", () => {
|
||||
const params = createRuleParams("minValue", "5");
|
||||
expect(params).toEqual({ min: 5 });
|
||||
});
|
||||
|
||||
test("should convert string number to number for maxValue", () => {
|
||||
const params = createRuleParams("maxValue", "50");
|
||||
expect(params).toEqual({ max: 50 });
|
||||
});
|
||||
|
||||
test("should convert string number to number for minSelections", () => {
|
||||
const params = createRuleParams("minSelections", "2");
|
||||
expect(params).toEqual({ min: 2 });
|
||||
});
|
||||
|
||||
test("should convert string number to number for maxSelections", () => {
|
||||
const params = createRuleParams("maxSelections", "5");
|
||||
expect(params).toEqual({ max: 5 });
|
||||
});
|
||||
|
||||
test("should handle invalid string number (defaults to 0 for minLength)", () => {
|
||||
const params = createRuleParams("minLength", "invalid");
|
||||
expect(params).toEqual({ min: 0 });
|
||||
});
|
||||
|
||||
test("should handle invalid string number (defaults to 100 for maxLength)", () => {
|
||||
const params = createRuleParams("maxLength", "invalid");
|
||||
expect(params).toEqual({ max: 100 });
|
||||
});
|
||||
|
||||
test("should handle invalid string number (defaults to 0 for minValue)", () => {
|
||||
const params = createRuleParams("minValue", "invalid");
|
||||
expect(params).toEqual({ min: 0 });
|
||||
});
|
||||
|
||||
test("should handle invalid string number (defaults to 100 for maxValue)", () => {
|
||||
const params = createRuleParams("maxValue", "invalid");
|
||||
expect(params).toEqual({ max: 100 });
|
||||
});
|
||||
|
||||
test("should handle invalid string number (defaults to 1 for minSelections)", () => {
|
||||
const params = createRuleParams("minSelections", "invalid");
|
||||
expect(params).toEqual({ min: 1 });
|
||||
});
|
||||
|
||||
test("should handle invalid string number (defaults to 3 for maxSelections)", () => {
|
||||
const params = createRuleParams("maxSelections", "invalid");
|
||||
expect(params).toEqual({ max: 3 });
|
||||
});
|
||||
});
|
||||
74
apps/web/modules/survey/editor/lib/validation-rules-utils.ts
Normal file
74
apps/web/modules/survey/editor/lib/validation-rules-utils.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
||||
import {
|
||||
APPLICABLE_RULES,
|
||||
TValidationRule,
|
||||
TValidationRuleType,
|
||||
} from "@formbricks/types/surveys/validation-rules";
|
||||
|
||||
/**
|
||||
* Get available rule types for an element type, excluding already added rules
|
||||
*/
|
||||
export const getAvailableRuleTypes = (
|
||||
elementType: TSurveyElementTypeEnum,
|
||||
existingRules: TValidationRule[]
|
||||
): TValidationRuleType[] => {
|
||||
const elementTypeKey = elementType.toString();
|
||||
const applicable = APPLICABLE_RULES[elementTypeKey] ?? [];
|
||||
|
||||
// Filter out rules that are already added (for non-repeatable rules)
|
||||
const existingTypes = new Set(existingRules.map((r) => r.type));
|
||||
|
||||
return applicable.filter((ruleType) => {
|
||||
// Allow only one of each rule type
|
||||
return !existingTypes.has(ruleType);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the value from rule params based on rule type
|
||||
*/
|
||||
export const getRuleValue = (rule: TValidationRule): number | string | undefined => {
|
||||
const params = rule.params as Record<string, unknown>;
|
||||
if ("min" in params) return params.min as number;
|
||||
if ("max" in params) return params.max as number;
|
||||
if ("pattern" in params) {
|
||||
const pattern = params.pattern as string;
|
||||
return pattern ?? "";
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create params object from rule type and value (without type field)
|
||||
*/
|
||||
export const createRuleParams = (
|
||||
ruleType: TValidationRuleType,
|
||||
value?: number | string
|
||||
): TValidationRule["params"] => {
|
||||
switch (ruleType) {
|
||||
case "required":
|
||||
return {};
|
||||
case "minLength":
|
||||
return { min: Number(value) || 0 };
|
||||
case "maxLength":
|
||||
return { max: Number(value) || 100 };
|
||||
case "pattern":
|
||||
return { pattern: value === undefined || value === null ? "" : String(value) };
|
||||
case "email":
|
||||
return {};
|
||||
case "url":
|
||||
return {};
|
||||
case "phone":
|
||||
return {};
|
||||
case "minValue":
|
||||
return { min: Number(value) || 0 };
|
||||
case "maxValue":
|
||||
return { max: Number(value) || 100 };
|
||||
case "minSelections":
|
||||
return { min: Number(value) || 1 };
|
||||
case "maxSelections":
|
||||
return { max: Number(value) || 3 };
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
@@ -29,6 +29,7 @@ export function AddressElement({
|
||||
}: Readonly<AddressElementProps>) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isCurrent = element.id === currentElementId;
|
||||
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||
|
||||
@@ -117,7 +118,7 @@ export function AddressElement({
|
||||
fields={formFields}
|
||||
value={convertToValueObject(value)}
|
||||
onChange={handleChange}
|
||||
required={element.required}
|
||||
required={isRequired}
|
||||
dir={dir}
|
||||
imageUrl={element.imageUrl}
|
||||
videoUrl={element.videoUrl}
|
||||
|
||||
@@ -32,6 +32,7 @@ export function CalElement({
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
||||
|
||||
const onSuccessfulBooking = useCallback(() => {
|
||||
@@ -46,7 +47,7 @@ export function CalElement({
|
||||
key={element.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (element.required && !value) {
|
||||
if (isRequired && !value) {
|
||||
setErrorMessage(t("errors.please_book_an_appointment"));
|
||||
return;
|
||||
}
|
||||
@@ -62,7 +63,7 @@ export function CalElement({
|
||||
<Headline
|
||||
headline={getLocalizedValue(element.headline, languageCode)}
|
||||
elementId={element.id}
|
||||
required={element.required}
|
||||
validationRules={element.validationRules}
|
||||
/>
|
||||
<Subheader
|
||||
subheader={element.subheader ? getLocalizedValue(element.subheader, languageCode) : ""}
|
||||
|
||||
@@ -31,6 +31,7 @@ export function ConsentElement({
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||
const isCurrent = element.id === currentElementId;
|
||||
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -40,7 +41,7 @@ export function ConsentElement({
|
||||
};
|
||||
|
||||
const validateRequired = (): boolean => {
|
||||
if (element.required && value !== "accepted") {
|
||||
if (isRequired && value !== "accepted") {
|
||||
setErrorMessage(t("errors.please_fill_out_this_field"));
|
||||
return false;
|
||||
}
|
||||
@@ -65,7 +66,7 @@ export function ConsentElement({
|
||||
checkboxLabel={getLocalizedValue(element.label, languageCode)}
|
||||
value={value === "accepted"}
|
||||
onChange={handleChange}
|
||||
required={element.required}
|
||||
required={isRequired}
|
||||
errorMessage={errorMessage}
|
||||
dir={dir}
|
||||
imageUrl={element.imageUrl}
|
||||
|
||||
@@ -30,6 +30,7 @@ export function ContactInfoElement({
|
||||
}: Readonly<ContactInfoElementProps>) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isCurrent = element.id === currentElementId;
|
||||
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||
|
||||
@@ -113,7 +114,7 @@ export function ContactInfoElement({
|
||||
fields={formFields}
|
||||
value={convertToValueObject(value)}
|
||||
onChange={handleChange}
|
||||
required={element.required}
|
||||
required={isRequired}
|
||||
dir={dir}
|
||||
imageUrl={element.imageUrl}
|
||||
videoUrl={element.videoUrl}
|
||||
|
||||
@@ -30,6 +30,7 @@ export function DateElement({
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||
const isCurrent = element.id === currentElementId;
|
||||
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||
const { t } = useTranslation();
|
||||
@@ -41,7 +42,7 @@ export function DateElement({
|
||||
};
|
||||
|
||||
const validateRequired = (): boolean => {
|
||||
if (element.required && (!value || value.trim() === "")) {
|
||||
if (isRequired && (!value || value.trim() === "")) {
|
||||
setErrorMessage(t("errors.please_select_a_date"));
|
||||
return false;
|
||||
}
|
||||
@@ -75,7 +76,7 @@ export function DateElement({
|
||||
onChange={handleChange}
|
||||
minDate={getMinDate()}
|
||||
maxDate={getMaxDate()}
|
||||
required={element.required}
|
||||
required={isRequired}
|
||||
errorMessage={errorMessage}
|
||||
locale={languageCode}
|
||||
imageUrl={element.imageUrl}
|
||||
|
||||
@@ -37,6 +37,7 @@ export function FileUploadElement({
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const isCurrent = element.id === currentElementId;
|
||||
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||
const { t } = useTranslation();
|
||||
@@ -326,7 +327,7 @@ export function FileUploadElement({
|
||||
);
|
||||
|
||||
const validateRequired = (): boolean => {
|
||||
if (element.required && (!value || value.length === 0)) {
|
||||
if (isRequired && (!value || value.length === 0)) {
|
||||
setErrorMessage(t("errors.please_upload_a_file"));
|
||||
return false;
|
||||
}
|
||||
@@ -353,7 +354,7 @@ export function FileUploadElement({
|
||||
onFileSelect={handleFileSelect}
|
||||
allowMultiple={element.allowMultipleFiles}
|
||||
allowedFileExtensions={element.allowedFileExtensions}
|
||||
required={element.required}
|
||||
required={isRequired}
|
||||
errorMessage={errorMessage}
|
||||
isUploading={isUploading}
|
||||
imageUrl={element.imageUrl}
|
||||
|
||||
@@ -29,6 +29,7 @@ export function MatrixElement({
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||
const isCurrent = element.id === currentElementId;
|
||||
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||
const { t } = useTranslation();
|
||||
@@ -121,7 +122,7 @@ export function MatrixElement({
|
||||
};
|
||||
|
||||
const validateRequired = (): boolean => {
|
||||
if (element.required) {
|
||||
if (isRequired) {
|
||||
const hasUnansweredRows = rows.some((row) => !value[row.label]);
|
||||
if (hasUnansweredRows) {
|
||||
setErrorMessage(t("errors.please_select_an_option"));
|
||||
@@ -150,7 +151,7 @@ export function MatrixElement({
|
||||
columns={columns}
|
||||
value={convertValueToIds(value)}
|
||||
onChange={handleChange}
|
||||
required={element.required}
|
||||
required={isRequired}
|
||||
errorMessage={errorMessage}
|
||||
imageUrl={element.imageUrl}
|
||||
videoUrl={element.videoUrl}
|
||||
|
||||
@@ -33,6 +33,7 @@ export function MultipleChoiceMultiElement({
|
||||
const [otherValue, setOtherValue] = useState("");
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||
const isCurrent = element.id === currentElementId;
|
||||
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||
const { t } = useTranslation();
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||
|
||||
@@ -174,11 +175,11 @@ export function MultipleChoiceMultiElement({
|
||||
};
|
||||
|
||||
const validateRequired = (): boolean => {
|
||||
if (element.required && (!Array.isArray(value) || value.length === 0)) {
|
||||
if (isRequired && (!Array.isArray(value) || value.length === 0)) {
|
||||
setErrorMessage(t("errors.please_select_an_option"));
|
||||
return false;
|
||||
}
|
||||
if (element.required && isOtherSelected && !otherValue.trim()) {
|
||||
if (isRequired && isOtherSelected && !otherValue.trim()) {
|
||||
setErrorMessage(t("errors.please_fill_out_this_field"));
|
||||
return false;
|
||||
}
|
||||
@@ -262,7 +263,7 @@ export function MultipleChoiceMultiElement({
|
||||
options={allOptions}
|
||||
value={selectedValues}
|
||||
onChange={handleMultiSelectChange}
|
||||
required={element.required}
|
||||
required={isRequired}
|
||||
errorMessage={errorMessage}
|
||||
dir={dir}
|
||||
otherOptionId={otherOption?.id}
|
||||
|
||||
@@ -33,6 +33,7 @@ export function MultipleChoiceSingleElement({
|
||||
const [otherValue, setOtherValue] = useState("");
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||
const isCurrent = element.id === currentElementId;
|
||||
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -154,12 +155,12 @@ export function MultipleChoiceSingleElement({
|
||||
|
||||
const validateRequired = (): boolean => {
|
||||
// Check if nothing is selected
|
||||
if (element.required && selectedValue === undefined) {
|
||||
if (isRequired && selectedValue === undefined) {
|
||||
setErrorMessage(t("errors.please_select_an_option"));
|
||||
return false;
|
||||
}
|
||||
// Check if "other" is selected but not filled
|
||||
if (element.required && isOtherSelected && !otherValue.trim()) {
|
||||
if (isRequired && isOtherSelected && !otherValue.trim()) {
|
||||
setErrorMessage(t("errors.please_fill_out_this_field"));
|
||||
return false;
|
||||
}
|
||||
@@ -184,7 +185,7 @@ export function MultipleChoiceSingleElement({
|
||||
options={allOptions}
|
||||
value={selectedValue}
|
||||
onChange={handleChange}
|
||||
required={element.required}
|
||||
required={isRequired}
|
||||
errorMessage={errorMessage}
|
||||
dir={dir}
|
||||
otherOptionId={otherOption?.id}
|
||||
|
||||
@@ -32,6 +32,7 @@ export function NPSElement({
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||
const isCurrent = element.id === currentElementId;
|
||||
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -43,7 +44,7 @@ export function NPSElement({
|
||||
};
|
||||
|
||||
const validateRequired = (): boolean => {
|
||||
if (element.required && value === undefined) {
|
||||
if (isRequired && value === undefined) {
|
||||
setErrorMessage(t("errors.please_select_an_option"));
|
||||
return false;
|
||||
}
|
||||
@@ -70,7 +71,7 @@ export function NPSElement({
|
||||
lowerLabel={getLocalizedValue(element.lowerLabel, languageCode)}
|
||||
upperLabel={getLocalizedValue(element.upperLabel, languageCode)}
|
||||
colorCoding={element.isColorCodingEnabled}
|
||||
required={element.required}
|
||||
required={isRequired}
|
||||
errorMessage={errorMessage}
|
||||
dir={dir}
|
||||
imageUrl={element.imageUrl}
|
||||
|
||||
@@ -33,6 +33,7 @@ export function OpenTextElement({
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||
const isCurrent = element.id === currentElementId;
|
||||
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -43,7 +44,7 @@ export function OpenTextElement({
|
||||
};
|
||||
|
||||
const validateRequired = (): boolean => {
|
||||
if (element.required && (!value || value.trim() === "")) {
|
||||
if (isRequired && (!value || value.trim() === "")) {
|
||||
setErrorMessage(t("errors.please_fill_out_this_field"));
|
||||
return false;
|
||||
}
|
||||
@@ -122,7 +123,7 @@ export function OpenTextElement({
|
||||
placeholder={getLocalizedValue(element.placeholder, languageCode)}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
required={element.required}
|
||||
required={isRequired}
|
||||
longAnswer={element.longAnswer !== false}
|
||||
inputType={getInputType()}
|
||||
charLimit={element.inputType === "text" ? element.charLimit : undefined}
|
||||
|
||||
@@ -32,6 +32,7 @@ export function PictureSelectionElement({
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isCurrent = element.id === currentElementId;
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||
const { t } = useTranslation();
|
||||
// Convert choices to PictureSelectOption format
|
||||
@@ -66,7 +67,7 @@ export function PictureSelectionElement({
|
||||
|
||||
const handleSubmit = (e: Event) => {
|
||||
e.preventDefault();
|
||||
if (element.required) {
|
||||
if (isRequired) {
|
||||
if (element.allowMulti) {
|
||||
if (!currentValue || !Array.isArray(currentValue) || currentValue.length === 0) {
|
||||
setErrorMessage(t("errors.please_select_an_option"));
|
||||
@@ -94,7 +95,7 @@ export function PictureSelectionElement({
|
||||
value={currentValue}
|
||||
onChange={handleChange}
|
||||
allowMulti={element.allowMulti}
|
||||
required={element.required}
|
||||
required={isRequired}
|
||||
dir={dir}
|
||||
errorMessage={errorMessage}
|
||||
imageUrl={element.imageUrl}
|
||||
|
||||
@@ -31,6 +31,7 @@ export function RankingElement({
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||
const isCurrent = element.id === currentElementId;
|
||||
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||
|
||||
@@ -109,7 +110,7 @@ export function RankingElement({
|
||||
const isValueArray = Array.isArray(value);
|
||||
const allItemsRanked = isValueArray && value.length === element.choices.length;
|
||||
|
||||
if ((element.required && !allItemsRanked) || (!element.required && value.length > 0 && !allItemsRanked)) {
|
||||
if ((isRequired && !allItemsRanked) || (!isRequired && value.length > 0 && !allItemsRanked)) {
|
||||
setErrorMessage(t("errors.please_rank_all_items_before_submitting"));
|
||||
return false;
|
||||
}
|
||||
@@ -135,7 +136,7 @@ export function RankingElement({
|
||||
options={options}
|
||||
value={selectedValues}
|
||||
onChange={handleChange}
|
||||
required={element.required}
|
||||
required={isRequired}
|
||||
errorMessage={errorMessage}
|
||||
imageUrl={element.imageUrl}
|
||||
videoUrl={element.videoUrl}
|
||||
|
||||
@@ -30,6 +30,7 @@ export function RatingElement({
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
const isCurrent = element.id === currentElementId;
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -41,7 +42,7 @@ export function RatingElement({
|
||||
};
|
||||
|
||||
const validateRequired = (): boolean => {
|
||||
if (element.required && !value) {
|
||||
if (isRequired && !value) {
|
||||
setErrorMessage(t("errors.please_select_an_option"));
|
||||
return false;
|
||||
}
|
||||
@@ -69,7 +70,7 @@ export function RatingElement({
|
||||
lowerLabel={getLocalizedValue(element.lowerLabel, languageCode)}
|
||||
upperLabel={getLocalizedValue(element.upperLabel, languageCode)}
|
||||
colorCoding={element.isColorCodingEnabled}
|
||||
required={element.required}
|
||||
required={isRequired}
|
||||
dir={dir}
|
||||
imageUrl={element.imageUrl}
|
||||
videoUrl={element.videoUrl}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TValidationRule } from "@formbricks/types/surveys/validation-rules";
|
||||
import { isValidHTML, stripInlineStyles } from "@/lib/html-utils";
|
||||
|
||||
interface HeadlineProps {
|
||||
headline: string;
|
||||
elementId: string;
|
||||
required?: boolean;
|
||||
validationRules?: TValidationRule[];
|
||||
alignTextCenter?: boolean;
|
||||
}
|
||||
|
||||
export function Headline({ headline, elementId, required = true, alignTextCenter = false }: HeadlineProps) {
|
||||
export function Headline({
|
||||
headline,
|
||||
elementId,
|
||||
validationRules,
|
||||
alignTextCenter = false,
|
||||
}: Readonly<HeadlineProps>) {
|
||||
const hasRequiredRule = validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||
const { t } = useTranslation();
|
||||
const isQuestionCard = elementId !== "EndingCard" && elementId !== "welcomeCard";
|
||||
// Strip inline styles BEFORE parsing to avoid CSP violations
|
||||
@@ -18,14 +25,14 @@ export function Headline({ headline, elementId, required = true, alignTextCenter
|
||||
const safeHtml =
|
||||
isHeadlineHtml && strippedHeadline
|
||||
? DOMPurify.sanitize(strippedHeadline, {
|
||||
ADD_ATTR: ["target"],
|
||||
FORBID_ATTR: ["style"], // Additional safeguard to remove any remaining inline styles
|
||||
})
|
||||
ADD_ATTR: ["target"],
|
||||
FORBID_ATTR: ["style"], // Additional safeguard to remove any remaining inline styles
|
||||
})
|
||||
: "";
|
||||
|
||||
return (
|
||||
<label htmlFor={elementId} className="text-heading mb-[3px] flex flex-col">
|
||||
{required && isQuestionCard && (
|
||||
{hasRequiredRule && isQuestionCard && (
|
||||
<span
|
||||
className="mb-[3px] text-xs leading-6 font-normal opacity-60"
|
||||
tabIndex={-1}
|
||||
|
||||
@@ -3,6 +3,22 @@ import { ZUrl } from "../common";
|
||||
import { ZI18nString } from "../i18n";
|
||||
import { ZAllowedFileExtension } from "../storage";
|
||||
import { FORBIDDEN_IDS } from "./validation";
|
||||
import {
|
||||
ZValidationRulesForAddress,
|
||||
ZValidationRulesForCal,
|
||||
ZValidationRulesForConsent,
|
||||
ZValidationRulesForContactInfo,
|
||||
ZValidationRulesForDate,
|
||||
ZValidationRulesForFileUpload,
|
||||
ZValidationRulesForMatrix,
|
||||
ZValidationRulesForMultipleChoiceMulti,
|
||||
ZValidationRulesForMultipleChoiceSingle,
|
||||
ZValidationRulesForNPS,
|
||||
ZValidationRulesForOpenText,
|
||||
ZValidationRulesForPictureSelection,
|
||||
ZValidationRulesForRanking,
|
||||
ZValidationRulesForRating,
|
||||
} from "./validation-rules";
|
||||
|
||||
// Element Type Enum (same as question types)
|
||||
export enum TSurveyElementTypeEnum {
|
||||
@@ -50,6 +66,7 @@ export const ZSurveyElementId = z.string().superRefine((id, ctx) => {
|
||||
export type TSurveyElementId = z.infer<typeof ZSurveyElementId>;
|
||||
|
||||
// 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({
|
||||
id: ZSurveyElementId,
|
||||
type: z.nativeEnum(TSurveyElementTypeEnum),
|
||||
@@ -80,6 +97,7 @@ export const ZSurveyOpenTextElement = ZSurveyElementBase.extend({
|
||||
max: z.number().optional(),
|
||||
})
|
||||
.default({ enabled: false }),
|
||||
validationRules: ZValidationRulesForOpenText.optional(),
|
||||
}).superRefine((data, ctx) => {
|
||||
if (data.charLimit.enabled && data.charLimit.min === undefined && data.charLimit.max === undefined) {
|
||||
ctx.addIssue({
|
||||
@@ -116,6 +134,7 @@ export type TSurveyOpenTextElement = z.infer<typeof ZSurveyOpenTextElement>;
|
||||
export const ZSurveyConsentElement = ZSurveyElementBase.extend({
|
||||
type: z.literal(TSurveyElementTypeEnum.Consent),
|
||||
label: ZI18nString,
|
||||
validationRules: ZValidationRulesForConsent.optional(),
|
||||
});
|
||||
|
||||
export type TSurveyConsentElement = z.infer<typeof ZSurveyConsentElement>;
|
||||
@@ -131,18 +150,34 @@ export type TSurveyElementChoice = z.infer<typeof ZSurveyElementChoice>;
|
||||
export const ZShuffleOption = z.enum(["none", "all", "exceptLast"]);
|
||||
export type TShuffleOption = z.infer<typeof ZShuffleOption>;
|
||||
|
||||
export const ZSurveyMultipleChoiceElement = ZSurveyElementBase.extend({
|
||||
type: z.union([
|
||||
z.literal(TSurveyElementTypeEnum.MultipleChoiceSingle),
|
||||
z.literal(TSurveyElementTypeEnum.MultipleChoiceMulti),
|
||||
]),
|
||||
// Multiple Choice Single Element
|
||||
export const ZSurveyMultipleChoiceSingleElement = ZSurveyElementBase.extend({
|
||||
type: z.literal(TSurveyElementTypeEnum.MultipleChoiceSingle),
|
||||
choices: z
|
||||
.array(ZSurveyElementChoice)
|
||||
.min(2, { message: "Multiple Choice Element must have at least two choices" }),
|
||||
shuffleOption: ZShuffleOption.optional(),
|
||||
otherOptionPlaceholder: ZI18nString.optional(),
|
||||
validationRules: ZValidationRulesForMultipleChoiceSingle.optional(),
|
||||
});
|
||||
|
||||
// Multiple Choice Multi Element
|
||||
export const ZSurveyMultipleChoiceMultiElement = ZSurveyElementBase.extend({
|
||||
type: z.literal(TSurveyElementTypeEnum.MultipleChoiceMulti),
|
||||
choices: z
|
||||
.array(ZSurveyElementChoice)
|
||||
.min(2, { message: "Multiple Choice Element must have at least two choices" }),
|
||||
shuffleOption: ZShuffleOption.optional(),
|
||||
otherOptionPlaceholder: ZI18nString.optional(),
|
||||
validationRules: ZValidationRulesForMultipleChoiceMulti.optional(),
|
||||
});
|
||||
|
||||
// Union type for Multiple Choice Elements
|
||||
export const ZSurveyMultipleChoiceElement = z.union([
|
||||
ZSurveyMultipleChoiceSingleElement,
|
||||
ZSurveyMultipleChoiceMultiElement,
|
||||
]);
|
||||
|
||||
export type TSurveyMultipleChoiceElement = z.infer<typeof ZSurveyMultipleChoiceElement>;
|
||||
|
||||
// NPS Element
|
||||
@@ -151,6 +186,7 @@ export const ZSurveyNPSElement = ZSurveyElementBase.extend({
|
||||
lowerLabel: ZI18nString.optional(),
|
||||
upperLabel: ZI18nString.optional(),
|
||||
isColorCodingEnabled: z.boolean().optional().default(false),
|
||||
validationRules: ZValidationRulesForNPS.optional(),
|
||||
});
|
||||
|
||||
export type TSurveyNPSElement = z.infer<typeof ZSurveyNPSElement>;
|
||||
@@ -202,6 +238,7 @@ export const ZSurveyRatingElement = ZSurveyElementBase.extend({
|
||||
lowerLabel: ZI18nString.optional(),
|
||||
upperLabel: ZI18nString.optional(),
|
||||
isColorCodingEnabled: z.boolean().optional().default(false),
|
||||
validationRules: ZValidationRulesForRating.optional(),
|
||||
});
|
||||
|
||||
export type TSurveyRatingElement = z.infer<typeof ZSurveyRatingElement>;
|
||||
@@ -220,6 +257,7 @@ export const ZSurveyPictureSelectionElement = ZSurveyElementBase.extend({
|
||||
choices: z
|
||||
.array(ZSurveyPictureChoice)
|
||||
.min(2, { message: "Picture Selection element must have a minimum of 2 choices" }),
|
||||
validationRules: ZValidationRulesForPictureSelection.optional(),
|
||||
});
|
||||
|
||||
export type TSurveyPictureSelectionElement = z.infer<typeof ZSurveyPictureSelectionElement>;
|
||||
@@ -229,6 +267,7 @@ export const ZSurveyDateElement = ZSurveyElementBase.extend({
|
||||
type: z.literal(TSurveyElementTypeEnum.Date),
|
||||
html: ZI18nString.optional(),
|
||||
format: z.enum(["M-d-y", "d-M-y", "y-M-d"]),
|
||||
validationRules: ZValidationRulesForDate.optional(),
|
||||
});
|
||||
|
||||
export type TSurveyDateElement = z.infer<typeof ZSurveyDateElement>;
|
||||
@@ -239,6 +278,7 @@ export const ZSurveyFileUploadElement = ZSurveyElementBase.extend({
|
||||
allowMultipleFiles: z.boolean(),
|
||||
maxSizeInMB: z.number().optional(),
|
||||
allowedFileExtensions: z.array(ZAllowedFileExtension).optional(),
|
||||
validationRules: ZValidationRulesForFileUpload.optional(),
|
||||
});
|
||||
|
||||
export type TSurveyFileUploadElement = z.infer<typeof ZSurveyFileUploadElement>;
|
||||
@@ -248,6 +288,7 @@ export const ZSurveyCalElement = ZSurveyElementBase.extend({
|
||||
type: z.literal(TSurveyElementTypeEnum.Cal),
|
||||
calUserName: z.string().min(1, { message: "Cal user name is required" }),
|
||||
calHost: z.string().optional(),
|
||||
validationRules: ZValidationRulesForCal.optional(),
|
||||
});
|
||||
|
||||
export type TSurveyCalElement = z.infer<typeof ZSurveyCalElement>;
|
||||
@@ -265,6 +306,7 @@ export const ZSurveyMatrixElement = ZSurveyElementBase.extend({
|
||||
rows: z.array(ZSurveyMatrixElementChoice),
|
||||
columns: z.array(ZSurveyMatrixElementChoice),
|
||||
shuffleOption: ZShuffleOption.optional().default("none"),
|
||||
validationRules: ZValidationRulesForMatrix.optional(),
|
||||
});
|
||||
|
||||
export type TSurveyMatrixElement = z.infer<typeof ZSurveyMatrixElement>;
|
||||
@@ -286,6 +328,7 @@ export const ZSurveyAddressElement = ZSurveyElementBase.extend({
|
||||
state: ZToggleInputConfig,
|
||||
zip: ZToggleInputConfig,
|
||||
country: ZToggleInputConfig,
|
||||
validationRules: ZValidationRulesForAddress.optional(),
|
||||
});
|
||||
|
||||
export type TSurveyAddressElement = z.infer<typeof ZSurveyAddressElement>;
|
||||
@@ -299,6 +342,7 @@ export const ZSurveyRankingElement = ZSurveyElementBase.extend({
|
||||
.max(25, { message: "Ranking Element can have at most 25 options" }),
|
||||
otherOptionPlaceholder: ZI18nString.optional(),
|
||||
shuffleOption: ZShuffleOption.optional(),
|
||||
validationRules: ZValidationRulesForRanking.optional(),
|
||||
});
|
||||
|
||||
export type TSurveyRankingElement = z.infer<typeof ZSurveyRankingElement>;
|
||||
@@ -311,6 +355,7 @@ export const ZSurveyContactInfoElement = ZSurveyElementBase.extend({
|
||||
email: ZToggleInputConfig,
|
||||
phone: ZToggleInputConfig,
|
||||
company: ZToggleInputConfig,
|
||||
validationRules: ZValidationRulesForContactInfo.optional(),
|
||||
});
|
||||
|
||||
export type TSurveyContactInfoElement = z.infer<typeof ZSurveyContactInfoElement>;
|
||||
@@ -319,7 +364,8 @@ export type TSurveyContactInfoElement = z.infer<typeof ZSurveyContactInfoElement
|
||||
export const ZSurveyElement = z.union([
|
||||
ZSurveyOpenTextElement,
|
||||
ZSurveyConsentElement,
|
||||
ZSurveyMultipleChoiceElement,
|
||||
ZSurveyMultipleChoiceSingleElement,
|
||||
ZSurveyMultipleChoiceMultiElement,
|
||||
ZSurveyNPSElement,
|
||||
ZSurveyCTAElement,
|
||||
ZSurveyRatingElement,
|
||||
|
||||
460
packages/types/surveys/validation-rules.ts
Normal file
460
packages/types/surveys/validation-rules.ts
Normal file
@@ -0,0 +1,460 @@
|
||||
import { z } from "zod";
|
||||
import { ZI18nString } from "../i18n";
|
||||
|
||||
// Validation rule type enum - extensible for future rule types
|
||||
export const ZValidationRuleType = z.enum([
|
||||
// Universal rules
|
||||
"required",
|
||||
|
||||
// Text/OpenText rules
|
||||
"minLength",
|
||||
"maxLength",
|
||||
"pattern",
|
||||
"email",
|
||||
"url",
|
||||
"phone",
|
||||
|
||||
// Numeric rules (for OpenText inputType=number)
|
||||
"minValue",
|
||||
"maxValue",
|
||||
|
||||
// Selection rules (MultiSelect, PictureSelection)
|
||||
"minSelections",
|
||||
"maxSelections",
|
||||
]);
|
||||
|
||||
export type TValidationRuleType = z.infer<typeof ZValidationRuleType>;
|
||||
|
||||
// Rule params - union for type-safe params per rule type (type is now at rule level)
|
||||
export const ZValidationRuleParamsRequired = z.object({});
|
||||
|
||||
export const ZValidationRuleParamsMinLength = z.object({
|
||||
min: z.number().min(0),
|
||||
});
|
||||
|
||||
export const ZValidationRuleParamsMaxLength = z.object({
|
||||
max: z.number().min(1),
|
||||
});
|
||||
|
||||
export const ZValidationRuleParamsPattern = z.object({
|
||||
pattern: z.string().min(1),
|
||||
flags: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ZValidationRuleParamsEmail = z.object({});
|
||||
|
||||
export const ZValidationRuleParamsUrl = z.object({});
|
||||
|
||||
export const ZValidationRuleParamsPhone = z.object({});
|
||||
|
||||
export const ZValidationRuleParamsMinValue = z.object({
|
||||
min: z.number(),
|
||||
});
|
||||
|
||||
export const ZValidationRuleParamsMaxValue = z.object({
|
||||
max: z.number(),
|
||||
});
|
||||
|
||||
export const ZValidationRuleParamsMinSelections = z.object({
|
||||
min: z.number().min(1),
|
||||
});
|
||||
|
||||
export const ZValidationRuleParamsMaxSelections = z.object({
|
||||
max: z.number().min(1),
|
||||
});
|
||||
|
||||
// Union of all params types
|
||||
export const ZValidationRuleParams = z.union([
|
||||
ZValidationRuleParamsRequired,
|
||||
ZValidationRuleParamsMinLength,
|
||||
ZValidationRuleParamsMaxLength,
|
||||
ZValidationRuleParamsPattern,
|
||||
ZValidationRuleParamsEmail,
|
||||
ZValidationRuleParamsUrl,
|
||||
ZValidationRuleParamsPhone,
|
||||
ZValidationRuleParamsMinValue,
|
||||
ZValidationRuleParamsMaxValue,
|
||||
ZValidationRuleParamsMinSelections,
|
||||
ZValidationRuleParamsMaxSelections,
|
||||
]);
|
||||
|
||||
export type TValidationRuleParams = z.infer<typeof ZValidationRuleParams>;
|
||||
|
||||
// Extract specific param types for validators
|
||||
export type TValidationRuleParamsRequired = z.infer<typeof ZValidationRuleParamsRequired>;
|
||||
export type TValidationRuleParamsMinLength = z.infer<typeof ZValidationRuleParamsMinLength>;
|
||||
export type TValidationRuleParamsMaxLength = z.infer<typeof ZValidationRuleParamsMaxLength>;
|
||||
export type TValidationRuleParamsPattern = z.infer<typeof ZValidationRuleParamsPattern>;
|
||||
export type TValidationRuleParamsEmail = z.infer<typeof ZValidationRuleParamsEmail>;
|
||||
export type TValidationRuleParamsUrl = z.infer<typeof ZValidationRuleParamsUrl>;
|
||||
export type TValidationRuleParamsPhone = z.infer<typeof ZValidationRuleParamsPhone>;
|
||||
export type TValidationRuleParamsMinValue = z.infer<typeof ZValidationRuleParamsMinValue>;
|
||||
export type TValidationRuleParamsMaxValue = z.infer<typeof ZValidationRuleParamsMaxValue>;
|
||||
export type TValidationRuleParamsMinSelections = z.infer<typeof ZValidationRuleParamsMinSelections>;
|
||||
export type TValidationRuleParamsMaxSelections = z.infer<typeof ZValidationRuleParamsMaxSelections>;
|
||||
|
||||
// Validation rule stored on element - discriminated union with type at top level
|
||||
export const ZValidationRule = z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("required"),
|
||||
params: ZValidationRuleParamsRequired,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("minLength"),
|
||||
params: ZValidationRuleParamsMinLength,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("maxLength"),
|
||||
params: ZValidationRuleParamsMaxLength,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("pattern"),
|
||||
params: ZValidationRuleParamsPattern,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("email"),
|
||||
params: ZValidationRuleParamsEmail,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("url"),
|
||||
params: ZValidationRuleParamsUrl,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("phone"),
|
||||
params: ZValidationRuleParamsPhone,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("minValue"),
|
||||
params: ZValidationRuleParamsMinValue,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("maxValue"),
|
||||
params: ZValidationRuleParamsMaxValue,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("minSelections"),
|
||||
params: ZValidationRuleParamsMinSelections,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("maxSelections"),
|
||||
params: ZValidationRuleParamsMaxSelections,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type TValidationRule = z.infer<typeof ZValidationRule>;
|
||||
|
||||
// Array of validation rules
|
||||
export const ZValidationRules = z.array(ZValidationRule);
|
||||
export type TValidationRules = z.infer<typeof ZValidationRules>;
|
||||
|
||||
// Applicable rules per element type - const arrays for type inference (must be defined before types)
|
||||
const OPEN_TEXT_RULES = [
|
||||
"required",
|
||||
"minLength",
|
||||
"maxLength",
|
||||
"pattern",
|
||||
"email",
|
||||
"url",
|
||||
"phone",
|
||||
"minValue",
|
||||
"maxValue",
|
||||
] as const;
|
||||
|
||||
const MULTIPLE_CHOICE_SINGLE_RULES = ["required"] as const;
|
||||
const MULTIPLE_CHOICE_MULTI_RULES = ["required", "minSelections", "maxSelections"] as const;
|
||||
const RATING_RULES = ["required"] as const;
|
||||
const NPS_RULES = ["required"] as const;
|
||||
const DATE_RULES = ["required"] as const;
|
||||
const CONSENT_RULES = ["required"] as const;
|
||||
const MATRIX_RULES = ["required"] as const;
|
||||
const RANKING_RULES = ["required"] as const;
|
||||
const FILE_UPLOAD_RULES = ["required"] as const;
|
||||
const PICTURE_SELECTION_RULES = ["required", "minSelections", "maxSelections"] as const;
|
||||
const ADDRESS_RULES = ["required"] as const;
|
||||
const CONTACT_INFO_RULES = ["required"] as const;
|
||||
const CAL_RULES = ["required"] as const;
|
||||
const CTA_RULES = [] as const;
|
||||
|
||||
// Applicable rules per element type
|
||||
export const APPLICABLE_RULES: Record<string, TValidationRuleType[]> = {
|
||||
openText: [...OPEN_TEXT_RULES],
|
||||
multipleChoiceSingle: [...MULTIPLE_CHOICE_SINGLE_RULES],
|
||||
multipleChoiceMulti: [...MULTIPLE_CHOICE_MULTI_RULES],
|
||||
rating: [...RATING_RULES],
|
||||
nps: [...NPS_RULES],
|
||||
date: [...DATE_RULES],
|
||||
consent: [...CONSENT_RULES],
|
||||
matrix: [...MATRIX_RULES],
|
||||
ranking: [...RANKING_RULES],
|
||||
fileUpload: [...FILE_UPLOAD_RULES],
|
||||
pictureSelection: [...PICTURE_SELECTION_RULES],
|
||||
address: [...ADDRESS_RULES],
|
||||
contactInfo: [...CONTACT_INFO_RULES],
|
||||
cal: [...CAL_RULES],
|
||||
cta: [...CTA_RULES], // CTA never validates
|
||||
};
|
||||
|
||||
// Type helper to filter validation rules by allowed types
|
||||
export type TValidationRuleForElementType<T extends TValidationRuleType> = Extract<
|
||||
TValidationRule,
|
||||
{ type: T }
|
||||
>;
|
||||
|
||||
// Type helper to get validation rules array for specific element type
|
||||
export type TValidationRulesForElementType<T extends readonly TValidationRuleType[]> =
|
||||
TValidationRuleForElementType<T[number]>[];
|
||||
|
||||
// Specific validation rule types for each element type
|
||||
export type TValidationRulesForOpenText = TValidationRulesForElementType<typeof OPEN_TEXT_RULES>;
|
||||
export type TValidationRulesForMultipleChoiceSingle = TValidationRulesForElementType<
|
||||
typeof MULTIPLE_CHOICE_SINGLE_RULES
|
||||
>;
|
||||
export type TValidationRulesForMultipleChoiceMulti = TValidationRulesForElementType<
|
||||
typeof MULTIPLE_CHOICE_MULTI_RULES
|
||||
>;
|
||||
export type TValidationRulesForRating = TValidationRulesForElementType<typeof RATING_RULES>;
|
||||
export type TValidationRulesForNPS = TValidationRulesForElementType<typeof NPS_RULES>;
|
||||
export type TValidationRulesForDate = TValidationRulesForElementType<typeof DATE_RULES>;
|
||||
export type TValidationRulesForConsent = TValidationRulesForElementType<typeof CONSENT_RULES>;
|
||||
export type TValidationRulesForMatrix = TValidationRulesForElementType<typeof MATRIX_RULES>;
|
||||
export type TValidationRulesForRanking = TValidationRulesForElementType<typeof RANKING_RULES>;
|
||||
export type TValidationRulesForFileUpload = TValidationRulesForElementType<typeof FILE_UPLOAD_RULES>;
|
||||
export type TValidationRulesForPictureSelection = TValidationRulesForElementType<
|
||||
typeof PICTURE_SELECTION_RULES
|
||||
>;
|
||||
export type TValidationRulesForAddress = TValidationRulesForElementType<typeof ADDRESS_RULES>;
|
||||
export type TValidationRulesForContactInfo = TValidationRulesForElementType<typeof CONTACT_INFO_RULES>;
|
||||
export type TValidationRulesForCal = TValidationRulesForElementType<typeof CAL_RULES>;
|
||||
export type TValidationRulesForCTA = TValidationRulesForElementType<typeof CTA_RULES>;
|
||||
|
||||
// Element-specific validation rules schemas (manually created for type safety)
|
||||
// These are narrowed versions of ZValidationRule that only include applicable rule types
|
||||
export const ZValidationRulesForOpenText: z.ZodType<TValidationRulesForOpenText> = z.array(
|
||||
z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("required"),
|
||||
params: ZValidationRuleParamsRequired,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("minLength"),
|
||||
params: ZValidationRuleParamsMinLength,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("maxLength"),
|
||||
params: ZValidationRuleParamsMaxLength,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("pattern"),
|
||||
params: ZValidationRuleParamsPattern,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("email"),
|
||||
params: ZValidationRuleParamsEmail,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("url"),
|
||||
params: ZValidationRuleParamsUrl,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("phone"),
|
||||
params: ZValidationRuleParamsPhone,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("minValue"),
|
||||
params: ZValidationRuleParamsMinValue,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("maxValue"),
|
||||
params: ZValidationRuleParamsMaxValue,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
export const ZValidationRulesForMultipleChoiceSingle: z.ZodType<TValidationRulesForMultipleChoiceSingle> =
|
||||
z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("required"),
|
||||
params: ZValidationRuleParamsRequired,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
})
|
||||
);
|
||||
|
||||
export const ZValidationRulesForMultipleChoiceMulti: z.ZodType<TValidationRulesForMultipleChoiceMulti> =
|
||||
z.array(
|
||||
z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("required"),
|
||||
params: ZValidationRuleParamsRequired,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("minSelections"),
|
||||
params: ZValidationRuleParamsMinSelections,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("maxSelections"),
|
||||
params: ZValidationRuleParamsMaxSelections,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
export const ZValidationRulesForRating: z.ZodType<TValidationRulesForRating> = z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("required"),
|
||||
params: ZValidationRuleParamsRequired,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
})
|
||||
);
|
||||
|
||||
export const ZValidationRulesForNPS: z.ZodType<TValidationRulesForNPS> = z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("required"),
|
||||
params: ZValidationRuleParamsRequired,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
})
|
||||
);
|
||||
|
||||
export const ZValidationRulesForDate: z.ZodType<TValidationRulesForDate> = z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("required"),
|
||||
params: ZValidationRuleParamsRequired,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
})
|
||||
);
|
||||
|
||||
export const ZValidationRulesForConsent: z.ZodType<TValidationRulesForConsent> = z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("required"),
|
||||
params: ZValidationRuleParamsRequired,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
})
|
||||
);
|
||||
|
||||
export const ZValidationRulesForMatrix: z.ZodType<TValidationRulesForMatrix> = z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("required"),
|
||||
params: ZValidationRuleParamsRequired,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
})
|
||||
);
|
||||
|
||||
export const ZValidationRulesForRanking: z.ZodType<TValidationRulesForRanking> = z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("required"),
|
||||
params: ZValidationRuleParamsRequired,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
})
|
||||
);
|
||||
|
||||
export const ZValidationRulesForFileUpload: z.ZodType<TValidationRulesForFileUpload> = z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("required"),
|
||||
params: ZValidationRuleParamsRequired,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
})
|
||||
);
|
||||
|
||||
export const ZValidationRulesForPictureSelection: z.ZodType<TValidationRulesForPictureSelection> = z.array(
|
||||
z.discriminatedUnion("type", [
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("required"),
|
||||
params: ZValidationRuleParamsRequired,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("minSelections"),
|
||||
params: ZValidationRuleParamsMinSelections,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("maxSelections"),
|
||||
params: ZValidationRuleParamsMaxSelections,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
export const ZValidationRulesForAddress: z.ZodType<TValidationRulesForAddress> = z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("required"),
|
||||
params: ZValidationRuleParamsRequired,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
})
|
||||
);
|
||||
|
||||
export const ZValidationRulesForContactInfo: z.ZodType<TValidationRulesForContactInfo> = z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("required"),
|
||||
params: ZValidationRuleParamsRequired,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
})
|
||||
);
|
||||
|
||||
export const ZValidationRulesForCal: z.ZodType<TValidationRulesForCal> = z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.literal("required"),
|
||||
params: ZValidationRuleParamsRequired,
|
||||
customErrorMessage: ZI18nString.optional(),
|
||||
})
|
||||
);
|
||||
|
||||
export const ZValidationRulesForCTA: z.ZodType<TValidationRulesForCTA> = z.array(z.never());
|
||||
Reference in New Issue
Block a user