mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-24 11:39:22 -05:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 67161155a9 | |||
| 9b0cf5f532 | |||
| a32241d7c8 | |||
| a296ad189a | |||
| 942cb0f8d0 | |||
| 3e3b8cc349 |
+15
-5
@@ -211,7 +211,6 @@ checksums:
|
|||||||
common/imprint: c4e5f2a1994d3cc5896b200709cc499c
|
common/imprint: c4e5f2a1994d3cc5896b200709cc499c
|
||||||
common/in_progress: 3de9afebcb9d4ce8ac42e14995f79ffd
|
common/in_progress: 3de9afebcb9d4ce8ac42e14995f79ffd
|
||||||
common/inactive_surveys: 324b8e1844739cdc2a3bc71aef143a76
|
common/inactive_surveys: 324b8e1844739cdc2a3bc71aef143a76
|
||||||
common/input_type: df4865b5d0a598a8d7f563dcec104df5
|
|
||||||
common/integration: 40d02f65c4356003e0e90ffb944907d2
|
common/integration: 40d02f65c4356003e0e90ffb944907d2
|
||||||
common/integrations: 0ccce343287704cd90150c32e2fcad36
|
common/integrations: 0ccce343287704cd90150c32e2fcad36
|
||||||
common/invalid_date: 4c18c82f7317d4a02f8d5fef611e82b7
|
common/invalid_date: 4c18c82f7317d4a02f8d5fef611e82b7
|
||||||
@@ -235,13 +234,11 @@ checksums:
|
|||||||
common/look_and_feel: 9125503712626d495cedec7a79f1418c
|
common/look_and_feel: 9125503712626d495cedec7a79f1418c
|
||||||
common/manage: a3d40c0267b81ae53c9598eaeb05087d
|
common/manage: a3d40c0267b81ae53c9598eaeb05087d
|
||||||
common/marketing: fcf0f06f8b64b458c7ca6d95541a3cc8
|
common/marketing: fcf0f06f8b64b458c7ca6d95541a3cc8
|
||||||
common/maximum: 4c07541dd1f093775bdc61b559cca6c8
|
|
||||||
common/member: 1606dc30b369856b9dba1fe9aec425d2
|
common/member: 1606dc30b369856b9dba1fe9aec425d2
|
||||||
common/members: 0932e80cba1e3e0a7f52bb67ff31da32
|
common/members: 0932e80cba1e3e0a7f52bb67ff31da32
|
||||||
common/members_and_teams: bf5c3fadcb9fc23533ec1532b805ac08
|
common/members_and_teams: bf5c3fadcb9fc23533ec1532b805ac08
|
||||||
common/membership_not_found: 7ac63584af23396aace9992ad919ffd4
|
common/membership_not_found: 7ac63584af23396aace9992ad919ffd4
|
||||||
common/metadata: 695d4f7da261ba76e3be4de495491028
|
common/metadata: 695d4f7da261ba76e3be4de495491028
|
||||||
common/minimum: d9759235086d0169928b3c1401115e22
|
|
||||||
common/mobile_overlay_app_works_best_on_desktop: 4509f7bfbb4edbd42e534042d6cb7e72
|
common/mobile_overlay_app_works_best_on_desktop: 4509f7bfbb4edbd42e534042d6cb7e72
|
||||||
common/mobile_overlay_surveys_look_good: 6d73b635018b4a5a89cce58e1d2497f5
|
common/mobile_overlay_surveys_look_good: 6d73b635018b4a5a89cce58e1d2497f5
|
||||||
common/mobile_overlay_title: 42f52259b7527989fb3a3240f5352a8b
|
common/mobile_overlay_title: 42f52259b7527989fb3a3240f5352a8b
|
||||||
@@ -1149,8 +1146,6 @@ checksums:
|
|||||||
environments/surveys/edit/change_the_question_color_of_the_survey: ab6942138a8c5fc6c8c3b9f8dd95e980
|
environments/surveys/edit/change_the_question_color_of_the_survey: ab6942138a8c5fc6c8c3b9f8dd95e980
|
||||||
environments/surveys/edit/changes_saved: 90aab363c9e96eaa1295a997c48f97f6
|
environments/surveys/edit/changes_saved: 90aab363c9e96eaa1295a997c48f97f6
|
||||||
environments/surveys/edit/changing_survey_type_will_remove_existing_distribution_channels: 9ce817be04f13f2f0db981145ec48df4
|
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/checkbox_label: 12a07d6bdf38e283a2e95892ec49b7f8
|
||||||
environments/surveys/edit/choose_the_actions_which_trigger_the_survey: 773b311a148a112243f3b139506b9987
|
environments/surveys/edit/choose_the_actions_which_trigger_the_survey: 773b311a148a112243f3b139506b9987
|
||||||
environments/surveys/edit/choose_the_first_question_on_your_block: bdece06ca04f89d0c445ba1554dd5b80
|
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/upper_label: 1fa48bce3fade6ffc1a52d9fdddf9e17
|
||||||
environments/surveys/edit/url_filters: e524879d2eb74463d7fd06a7e0f53421
|
environments/surveys/edit/url_filters: e524879d2eb74463d7fd06a7e0f53421
|
||||||
environments/surveys/edit/url_not_supported: af8a753467c617b596aadef1aaaed664
|
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_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_is_used_in_quota_please_remove_it_from_quota_first: 0d36e5b2713f5450fe346e0af0aaa29c
|
||||||
environments/surveys/edit/variable_name_is_already_taken_please_choose_another: 6da42fe8733c6379158bce9a176f76d7
|
environments/surveys/edit/variable_name_is_already_taken_please_choose_another: 6da42fe8733c6379158bce9a176f76d7
|
||||||
|
|||||||
@@ -238,7 +238,6 @@
|
|||||||
"imprint": "Impressum",
|
"imprint": "Impressum",
|
||||||
"in_progress": "Im Gange",
|
"in_progress": "Im Gange",
|
||||||
"inactive_surveys": "Inaktive Umfragen",
|
"inactive_surveys": "Inaktive Umfragen",
|
||||||
"input_type": "Eingabetyp",
|
|
||||||
"integration": "Integration",
|
"integration": "Integration",
|
||||||
"integrations": "Integrationen",
|
"integrations": "Integrationen",
|
||||||
"invalid_date": "Ungültiges Datum",
|
"invalid_date": "Ungültiges Datum",
|
||||||
@@ -262,13 +261,11 @@
|
|||||||
"look_and_feel": "Darstellung",
|
"look_and_feel": "Darstellung",
|
||||||
"manage": "Verwalten",
|
"manage": "Verwalten",
|
||||||
"marketing": "Marketing",
|
"marketing": "Marketing",
|
||||||
"maximum": "Maximal",
|
|
||||||
"member": "Mitglied",
|
"member": "Mitglied",
|
||||||
"members": "Mitglieder",
|
"members": "Mitglieder",
|
||||||
"members_and_teams": "Mitglieder & Teams",
|
"members_and_teams": "Mitglieder & Teams",
|
||||||
"membership_not_found": "Mitgliedschaft nicht gefunden",
|
"membership_not_found": "Mitgliedschaft nicht gefunden",
|
||||||
"metadata": "Metadaten",
|
"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_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_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!",
|
"mobile_overlay_title": "Oops, Bildschirm zu klein erkannt!",
|
||||||
@@ -1220,8 +1217,6 @@
|
|||||||
"change_the_question_color_of_the_survey": "Fragefarbe der Umfrage ändern.",
|
"change_the_question_color_of_the_survey": "Fragefarbe der Umfrage ändern.",
|
||||||
"changes_saved": "Änderungen gespeichert.",
|
"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.\"",
|
"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",
|
"checkbox_label": "Checkbox-Beschriftung",
|
||||||
"choose_the_actions_which_trigger_the_survey": "Aktionen auswählen, die die Umfrage auslösen.",
|
"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",
|
"choose_the_first_question_on_your_block": "Wählen sie die erste frage in ihrem block",
|
||||||
@@ -1589,6 +1584,23 @@
|
|||||||
"upper_label": "Oberes Label",
|
"upper_label": "Oberes Label",
|
||||||
"url_filters": "URL-Filter",
|
"url_filters": "URL-Filter",
|
||||||
"url_not_supported": "URL nicht unterstützt",
|
"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_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_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.",
|
"variable_name_is_already_taken_please_choose_another": "Variablenname ist bereits vergeben, bitte wähle einen anderen.",
|
||||||
|
|||||||
@@ -238,7 +238,6 @@
|
|||||||
"imprint": "Imprint",
|
"imprint": "Imprint",
|
||||||
"in_progress": "In Progress",
|
"in_progress": "In Progress",
|
||||||
"inactive_surveys": "Inactive surveys",
|
"inactive_surveys": "Inactive surveys",
|
||||||
"input_type": "Input type",
|
|
||||||
"integration": "integration",
|
"integration": "integration",
|
||||||
"integrations": "Integrations",
|
"integrations": "Integrations",
|
||||||
"invalid_date": "Invalid date",
|
"invalid_date": "Invalid date",
|
||||||
@@ -262,13 +261,11 @@
|
|||||||
"look_and_feel": "Look & Feel",
|
"look_and_feel": "Look & Feel",
|
||||||
"manage": "Manage",
|
"manage": "Manage",
|
||||||
"marketing": "Marketing",
|
"marketing": "Marketing",
|
||||||
"maximum": "Maximum",
|
|
||||||
"member": "Member",
|
"member": "Member",
|
||||||
"members": "Members",
|
"members": "Members",
|
||||||
"members_and_teams": "Members & Teams",
|
"members_and_teams": "Members & Teams",
|
||||||
"membership_not_found": "Membership not found",
|
"membership_not_found": "Membership not found",
|
||||||
"metadata": "Metadata",
|
"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_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_surveys_look_good": "Don't worry – your surveys look great on every device and screen size!",
|
||||||
"mobile_overlay_title": "Oops, tiny screen detected!",
|
"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.",
|
"change_the_question_color_of_the_survey": "Change the question color of the survey.",
|
||||||
"changes_saved": "Changes saved.",
|
"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.",
|
"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",
|
"checkbox_label": "Checkbox Label",
|
||||||
"choose_the_actions_which_trigger_the_survey": "Choose the actions which trigger the survey.",
|
"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",
|
"choose_the_first_question_on_your_block": "Choose the first question on your Block",
|
||||||
@@ -1589,6 +1584,23 @@
|
|||||||
"upper_label": "Upper Label",
|
"upper_label": "Upper Label",
|
||||||
"url_filters": "URL Filters",
|
"url_filters": "URL Filters",
|
||||||
"url_not_supported": "URL not supported",
|
"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_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_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.",
|
"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_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)"
|
"usability_score_name": "System Usability Score (SUS)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,7 +238,6 @@
|
|||||||
"imprint": "Aviso legal",
|
"imprint": "Aviso legal",
|
||||||
"in_progress": "En progreso",
|
"in_progress": "En progreso",
|
||||||
"inactive_surveys": "Encuestas inactivas",
|
"inactive_surveys": "Encuestas inactivas",
|
||||||
"input_type": "Tipo de entrada",
|
|
||||||
"integration": "integración",
|
"integration": "integración",
|
||||||
"integrations": "Integraciones",
|
"integrations": "Integraciones",
|
||||||
"invalid_date": "Fecha no válida",
|
"invalid_date": "Fecha no válida",
|
||||||
@@ -262,13 +261,11 @@
|
|||||||
"look_and_feel": "Apariencia",
|
"look_and_feel": "Apariencia",
|
||||||
"manage": "Gestionar",
|
"manage": "Gestionar",
|
||||||
"marketing": "Marketing",
|
"marketing": "Marketing",
|
||||||
"maximum": "Máximo",
|
|
||||||
"member": "Miembro",
|
"member": "Miembro",
|
||||||
"members": "Miembros",
|
"members": "Miembros",
|
||||||
"members_and_teams": "Miembros y equipos",
|
"members_and_teams": "Miembros y equipos",
|
||||||
"membership_not_found": "Membresía no encontrada",
|
"membership_not_found": "Membresía no encontrada",
|
||||||
"metadata": "Metadatos",
|
"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_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_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!",
|
"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.",
|
"change_the_question_color_of_the_survey": "Cambiar el color de las preguntas de la encuesta.",
|
||||||
"changes_saved": "Cambios guardados.",
|
"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.",
|
"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",
|
"checkbox_label": "Etiqueta de casilla de verificación",
|
||||||
"choose_the_actions_which_trigger_the_survey": "Elige las acciones que activan la encuesta.",
|
"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",
|
"choose_the_first_question_on_your_block": "Elige la primera pregunta en tu bloque",
|
||||||
@@ -1589,6 +1584,23 @@
|
|||||||
"upper_label": "Etiqueta superior",
|
"upper_label": "Etiqueta superior",
|
||||||
"url_filters": "Filtros de URL",
|
"url_filters": "Filtros de URL",
|
||||||
"url_not_supported": "URL no compatible",
|
"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_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_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.",
|
"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",
|
"imprint": "Empreinte",
|
||||||
"in_progress": "En cours",
|
"in_progress": "En cours",
|
||||||
"inactive_surveys": "Sondages inactifs",
|
"inactive_surveys": "Sondages inactifs",
|
||||||
"input_type": "Type d'entrée",
|
|
||||||
"integration": "intégration",
|
"integration": "intégration",
|
||||||
"integrations": "Intégrations",
|
"integrations": "Intégrations",
|
||||||
"invalid_date": "Date invalide",
|
"invalid_date": "Date invalide",
|
||||||
@@ -262,13 +261,11 @@
|
|||||||
"look_and_feel": "Apparence",
|
"look_and_feel": "Apparence",
|
||||||
"manage": "Gérer",
|
"manage": "Gérer",
|
||||||
"marketing": "Marketing",
|
"marketing": "Marketing",
|
||||||
"maximum": "Max",
|
|
||||||
"member": "Membre",
|
"member": "Membre",
|
||||||
"members": "Membres",
|
"members": "Membres",
|
||||||
"members_and_teams": "Membres & Équipes",
|
"members_and_teams": "Membres & Équipes",
|
||||||
"membership_not_found": "Abonnement non trouvé",
|
"membership_not_found": "Abonnement non trouvé",
|
||||||
"metadata": "Métadonnées",
|
"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_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_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é!",
|
"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.",
|
"change_the_question_color_of_the_survey": "Vous pouvez modifier la couleur des questions d'une enquête.",
|
||||||
"changes_saved": "Modifications enregistrées.",
|
"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.",
|
"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",
|
"checkbox_label": "Étiquette de case à cocher",
|
||||||
"choose_the_actions_which_trigger_the_survey": "Choisissez les actions qui déclenchent l'enquête.",
|
"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",
|
"choose_the_first_question_on_your_block": "Choisissez la première question de votre bloc",
|
||||||
@@ -1589,6 +1584,23 @@
|
|||||||
"upper_label": "Étiquette supérieure",
|
"upper_label": "Étiquette supérieure",
|
||||||
"url_filters": "Filtres d'URL",
|
"url_filters": "Filtres d'URL",
|
||||||
"url_not_supported": "URL non supportée",
|
"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_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_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.",
|
"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": "企業情報",
|
"imprint": "企業情報",
|
||||||
"in_progress": "進行中",
|
"in_progress": "進行中",
|
||||||
"inactive_surveys": "非アクティブなフォーム",
|
"inactive_surveys": "非アクティブなフォーム",
|
||||||
"input_type": "入力タイプ",
|
|
||||||
"integration": "連携",
|
"integration": "連携",
|
||||||
"integrations": "連携",
|
"integrations": "連携",
|
||||||
"invalid_date": "無効な日付です",
|
"invalid_date": "無効な日付です",
|
||||||
@@ -262,13 +261,11 @@
|
|||||||
"look_and_feel": "デザイン",
|
"look_and_feel": "デザイン",
|
||||||
"manage": "管理",
|
"manage": "管理",
|
||||||
"marketing": "マーケティング",
|
"marketing": "マーケティング",
|
||||||
"maximum": "最大",
|
|
||||||
"member": "メンバー",
|
"member": "メンバー",
|
||||||
"members": "メンバー",
|
"members": "メンバー",
|
||||||
"members_and_teams": "メンバー&チーム",
|
"members_and_teams": "メンバー&チーム",
|
||||||
"membership_not_found": "メンバーシップが見つかりません",
|
"membership_not_found": "メンバーシップが見つかりません",
|
||||||
"metadata": "メタデータ",
|
"metadata": "メタデータ",
|
||||||
"minimum": "最小",
|
|
||||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks は より 大きな 画面 で最適に 作動します。 フォーム を 管理または 構築する には、 別の デバイス に 切り替える 必要が あります。",
|
"mobile_overlay_app_works_best_on_desktop": "Formbricks は より 大きな 画面 で最適に 作動します。 フォーム を 管理または 構築する には、 別の デバイス に 切り替える 必要が あります。",
|
||||||
"mobile_overlay_surveys_look_good": "ご安心ください - お使い の デバイス や 画面 サイズ に 関係なく、 フォーム は 素晴らしく 見えます!",
|
"mobile_overlay_surveys_look_good": "ご安心ください - お使い の デバイス や 画面 サイズ に 関係なく、 フォーム は 素晴らしく 見えます!",
|
||||||
"mobile_overlay_title": "おっと、 小さな 画面 が 検出されました!",
|
"mobile_overlay_title": "おっと、 小さな 画面 が 検出されました!",
|
||||||
@@ -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_survey_type_will_remove_existing_distribution_channels": "フォームの種類を変更すると、共有方法に影響します。回答者が現在のタイプのアクセスリンクをすでに持っている場合、切り替え後にアクセスを失う可能性があります。",
|
||||||
"character_limit_toggle_description": "回答の長さの上限・下限を設定します。",
|
|
||||||
"character_limit_toggle_title": "文字数制限を追加",
|
|
||||||
"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フィルター",
|
"url_filters": "URLフィルター",
|
||||||
"url_not_supported": "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_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_is_used_in_quota_please_remove_it_from_quota_first": "変数 \"{variableName}\" は \"{quotaName}\" クォータ で使用されています",
|
||||||
"variable_name_is_already_taken_please_choose_another": "変数名はすでに使用されています。別の名前を選択してください。",
|
"variable_name_is_already_taken_please_choose_another": "変数名はすでに使用されています。別の名前を選択してください。",
|
||||||
|
|||||||
@@ -238,7 +238,6 @@
|
|||||||
"imprint": "Afdruk",
|
"imprint": "Afdruk",
|
||||||
"in_progress": "In uitvoering",
|
"in_progress": "In uitvoering",
|
||||||
"inactive_surveys": "Inactieve enquêtes",
|
"inactive_surveys": "Inactieve enquêtes",
|
||||||
"input_type": "Invoertype",
|
|
||||||
"integration": "integratie",
|
"integration": "integratie",
|
||||||
"integrations": "Integraties",
|
"integrations": "Integraties",
|
||||||
"invalid_date": "Ongeldige datum",
|
"invalid_date": "Ongeldige datum",
|
||||||
@@ -262,13 +261,11 @@
|
|||||||
"look_and_feel": "Kijk & voel",
|
"look_and_feel": "Kijk & voel",
|
||||||
"manage": "Beheren",
|
"manage": "Beheren",
|
||||||
"marketing": "Marketing",
|
"marketing": "Marketing",
|
||||||
"maximum": "Maximaal",
|
|
||||||
"member": "Lid",
|
"member": "Lid",
|
||||||
"members": "Leden",
|
"members": "Leden",
|
||||||
"members_and_teams": "Leden & teams",
|
"members_and_teams": "Leden & teams",
|
||||||
"membership_not_found": "Lidmaatschap niet gevonden",
|
"membership_not_found": "Lidmaatschap niet gevonden",
|
||||||
"metadata": "Metagegevens",
|
"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_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_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!",
|
"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.",
|
"change_the_question_color_of_the_survey": "Verander de vraagkleur van de enquête.",
|
||||||
"changes_saved": "Wijzigingen opgeslagen.",
|
"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.",
|
"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",
|
"checkbox_label": "Selectievakje-label",
|
||||||
"choose_the_actions_which_trigger_the_survey": "Kies de acties die de enquête activeren.",
|
"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",
|
"choose_the_first_question_on_your_block": "Kies de eerste vraag in je blok",
|
||||||
@@ -1589,6 +1584,23 @@
|
|||||||
"upper_label": "Bovenste etiket",
|
"upper_label": "Bovenste etiket",
|
||||||
"url_filters": "URL-filters",
|
"url_filters": "URL-filters",
|
||||||
"url_not_supported": "URL niet ondersteund",
|
"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_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_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.",
|
"variable_name_is_already_taken_please_choose_another": "Variabelenaam is al in gebruik, kies een andere.",
|
||||||
|
|||||||
@@ -238,7 +238,6 @@
|
|||||||
"imprint": "impressão",
|
"imprint": "impressão",
|
||||||
"in_progress": "Em andamento",
|
"in_progress": "Em andamento",
|
||||||
"inactive_surveys": "Pesquisas inativas",
|
"inactive_surveys": "Pesquisas inativas",
|
||||||
"input_type": "Tipo de entrada",
|
|
||||||
"integration": "integração",
|
"integration": "integração",
|
||||||
"integrations": "Integrações",
|
"integrations": "Integrações",
|
||||||
"invalid_date": "Data inválida",
|
"invalid_date": "Data inválida",
|
||||||
@@ -262,13 +261,11 @@
|
|||||||
"look_and_feel": "Aparência e Experiência",
|
"look_and_feel": "Aparência e Experiência",
|
||||||
"manage": "gerenciar",
|
"manage": "gerenciar",
|
||||||
"marketing": "marketing",
|
"marketing": "marketing",
|
||||||
"maximum": "Máximo",
|
|
||||||
"member": "Membros",
|
"member": "Membros",
|
||||||
"members": "Membros",
|
"members": "Membros",
|
||||||
"members_and_teams": "Membros e equipes",
|
"members_and_teams": "Membros e equipes",
|
||||||
"membership_not_found": "Assinatura não encontrada",
|
"membership_not_found": "Assinatura não encontrada",
|
||||||
"metadata": "metadados",
|
"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_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_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!",
|
"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.",
|
"change_the_question_color_of_the_survey": "Muda a cor da pergunta da pesquisa.",
|
||||||
"changes_saved": "Mudanças salvas.",
|
"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.",
|
"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",
|
"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_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",
|
"choose_the_first_question_on_your_block": "Escolha a primeira pergunta do seu bloco",
|
||||||
@@ -1589,6 +1584,23 @@
|
|||||||
"upper_label": "Etiqueta Superior",
|
"upper_label": "Etiqueta Superior",
|
||||||
"url_filters": "Filtros de URL",
|
"url_filters": "Filtros de URL",
|
||||||
"url_not_supported": "URL não suportada",
|
"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_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_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.",
|
"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",
|
"imprint": "Impressão",
|
||||||
"in_progress": "Em Progresso",
|
"in_progress": "Em Progresso",
|
||||||
"inactive_surveys": "Inquéritos inativos",
|
"inactive_surveys": "Inquéritos inativos",
|
||||||
"input_type": "Tipo de entrada",
|
|
||||||
"integration": "integração",
|
"integration": "integração",
|
||||||
"integrations": "Integrações",
|
"integrations": "Integrações",
|
||||||
"invalid_date": "Data inválida",
|
"invalid_date": "Data inválida",
|
||||||
@@ -262,13 +261,11 @@
|
|||||||
"look_and_feel": "Aparência e Sensação",
|
"look_and_feel": "Aparência e Sensação",
|
||||||
"manage": "Gerir",
|
"manage": "Gerir",
|
||||||
"marketing": "Marketing",
|
"marketing": "Marketing",
|
||||||
"maximum": "Máximo",
|
|
||||||
"member": "Membro",
|
"member": "Membro",
|
||||||
"members": "Membros",
|
"members": "Membros",
|
||||||
"members_and_teams": "Membros e equipas",
|
"members_and_teams": "Membros e equipas",
|
||||||
"membership_not_found": "Associação não encontrada",
|
"membership_not_found": "Associação não encontrada",
|
||||||
"metadata": "Metadados",
|
"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_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_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!",
|
"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",
|
"change_the_question_color_of_the_survey": "Alterar a cor da pergunta do inquérito",
|
||||||
"changes_saved": "Alterações guardadas.",
|
"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.",
|
"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",
|
"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_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",
|
"choose_the_first_question_on_your_block": "Escolha a primeira pergunta no seu bloco",
|
||||||
@@ -1589,6 +1584,23 @@
|
|||||||
"upper_label": "Etiqueta Superior",
|
"upper_label": "Etiqueta Superior",
|
||||||
"url_filters": "Filtros de URL",
|
"url_filters": "Filtros de URL",
|
||||||
"url_not_supported": "URL não suportado",
|
"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_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_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.",
|
"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ă",
|
"imprint": "Amprentă",
|
||||||
"in_progress": "În progres",
|
"in_progress": "În progres",
|
||||||
"inactive_surveys": "Sondaje inactive",
|
"inactive_surveys": "Sondaje inactive",
|
||||||
"input_type": "Tipul de intrare",
|
|
||||||
"integration": "integrare",
|
"integration": "integrare",
|
||||||
"integrations": "Integrări",
|
"integrations": "Integrări",
|
||||||
"invalid_date": "Dată invalidă",
|
"invalid_date": "Dată invalidă",
|
||||||
@@ -262,13 +261,11 @@
|
|||||||
"look_and_feel": "Aspect și Comportament",
|
"look_and_feel": "Aspect și Comportament",
|
||||||
"manage": "Gestionați",
|
"manage": "Gestionați",
|
||||||
"marketing": "Marketing",
|
"marketing": "Marketing",
|
||||||
"maximum": "Maximum",
|
|
||||||
"member": "Membru",
|
"member": "Membru",
|
||||||
"members": "Membri",
|
"members": "Membri",
|
||||||
"members_and_teams": "Membri și echipe",
|
"members_and_teams": "Membri și echipe",
|
||||||
"membership_not_found": "Apartenența nu a fost găsită",
|
"membership_not_found": "Apartenența nu a fost găsită",
|
||||||
"metadata": "Metadate",
|
"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_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_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!",
|
"mobile_overlay_title": "Ups, ecran mic detectat!",
|
||||||
@@ -1220,8 +1217,6 @@
|
|||||||
"change_the_question_color_of_the_survey": "Schimbați culoarea întrebării chestionarului.",
|
"change_the_question_color_of_the_survey": "Schimbați culoarea întrebării chestionarului.",
|
||||||
"changes_saved": "Modificările au fost salvate",
|
"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.",
|
"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",
|
"checkbox_label": "Etichetă casetă de selectare",
|
||||||
"choose_the_actions_which_trigger_the_survey": "Alegeți acțiunile care declanșează sondajul.",
|
"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",
|
"choose_the_first_question_on_your_block": "Alege prima întrebare din blocul tău",
|
||||||
@@ -1589,6 +1584,23 @@
|
|||||||
"upper_label": "Etichetă superioară",
|
"upper_label": "Etichetă superioară",
|
||||||
"url_filters": "Filtre URL",
|
"url_filters": "Filtre URL",
|
||||||
"url_not_supported": "URL nesuportat",
|
"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_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_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.",
|
"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": "Выходные данные",
|
"imprint": "Выходные данные",
|
||||||
"in_progress": "В процессе",
|
"in_progress": "В процессе",
|
||||||
"inactive_surveys": "Неактивные опросы",
|
"inactive_surveys": "Неактивные опросы",
|
||||||
"input_type": "Тип ввода",
|
|
||||||
"integration": "интеграция",
|
"integration": "интеграция",
|
||||||
"integrations": "Интеграции",
|
"integrations": "Интеграции",
|
||||||
"invalid_date": "Неверная дата",
|
"invalid_date": "Неверная дата",
|
||||||
@@ -262,13 +261,11 @@
|
|||||||
"look_and_feel": "Внешний вид",
|
"look_and_feel": "Внешний вид",
|
||||||
"manage": "Управление",
|
"manage": "Управление",
|
||||||
"marketing": "Маркетинг",
|
"marketing": "Маркетинг",
|
||||||
"maximum": "Максимум",
|
|
||||||
"member": "Участник",
|
"member": "Участник",
|
||||||
"members": "Участники",
|
"members": "Участники",
|
||||||
"members_and_teams": "Участники и команды",
|
"members_and_teams": "Участники и команды",
|
||||||
"membership_not_found": "Участие не найдено",
|
"membership_not_found": "Участие не найдено",
|
||||||
"metadata": "Метаданные",
|
"metadata": "Метаданные",
|
||||||
"minimum": "Минимум",
|
|
||||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks лучше всего работает на большом экране. Для управления или создания опросов перейдите на другое устройство.",
|
"mobile_overlay_app_works_best_on_desktop": "Formbricks лучше всего работает на большом экране. Для управления или создания опросов перейдите на другое устройство.",
|
||||||
"mobile_overlay_surveys_look_good": "Не волнуйтесь — ваши опросы отлично выглядят на любом устройстве и экране!",
|
"mobile_overlay_surveys_look_good": "Не волнуйтесь — ваши опросы отлично выглядят на любом устройстве и экране!",
|
||||||
"mobile_overlay_title": "Ой, обнаружен маленький экран!",
|
"mobile_overlay_title": "Ой, обнаружен маленький экран!",
|
||||||
@@ -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_survey_type_will_remove_existing_distribution_channels": "Изменение типа опроса повлияет на способы его распространения. Если у респондентов уже есть ссылки для доступа к текущему типу, после смены они могут потерять доступ.",
|
||||||
"character_limit_toggle_description": "Ограничьте минимальную и максимальную длину ответа.",
|
|
||||||
"character_limit_toggle_title": "Добавить ограничения на количество символов",
|
|
||||||
"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",
|
"url_filters": "Фильтры URL",
|
||||||
"url_not_supported": "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_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_is_used_in_quota_please_remove_it_from_quota_first": "Переменная «{variableName}» используется в квоте «{quotaName}»",
|
||||||
"variable_name_is_already_taken_please_choose_another": "Это имя переменной уже занято, выберите другое.",
|
"variable_name_is_already_taken_please_choose_another": "Это имя переменной уже занято, выберите другое.",
|
||||||
|
|||||||
@@ -238,7 +238,6 @@
|
|||||||
"imprint": "Impressum",
|
"imprint": "Impressum",
|
||||||
"in_progress": "Pågående",
|
"in_progress": "Pågående",
|
||||||
"inactive_surveys": "Inaktiva enkäter",
|
"inactive_surveys": "Inaktiva enkäter",
|
||||||
"input_type": "Inmatningstyp",
|
|
||||||
"integration": "integration",
|
"integration": "integration",
|
||||||
"integrations": "Integrationer",
|
"integrations": "Integrationer",
|
||||||
"invalid_date": "Ogiltigt datum",
|
"invalid_date": "Ogiltigt datum",
|
||||||
@@ -262,13 +261,11 @@
|
|||||||
"look_and_feel": "Utseende",
|
"look_and_feel": "Utseende",
|
||||||
"manage": "Hantera",
|
"manage": "Hantera",
|
||||||
"marketing": "Marknadsföring",
|
"marketing": "Marknadsföring",
|
||||||
"maximum": "Maximum",
|
|
||||||
"member": "Medlem",
|
"member": "Medlem",
|
||||||
"members": "Medlemmar",
|
"members": "Medlemmar",
|
||||||
"members_and_teams": "Medlemmar och team",
|
"members_and_teams": "Medlemmar och team",
|
||||||
"membership_not_found": "Medlemskap hittades inte",
|
"membership_not_found": "Medlemskap hittades inte",
|
||||||
"metadata": "Metadata",
|
"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_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_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!",
|
"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.",
|
"change_the_question_color_of_the_survey": "Ändra enkätens frågefärg.",
|
||||||
"changes_saved": "Ändringar sparade.",
|
"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.",
|
"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",
|
"checkbox_label": "Kryssruteetikett",
|
||||||
"choose_the_actions_which_trigger_the_survey": "Välj de åtgärder som utlöser enkäten.",
|
"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",
|
"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",
|
"upper_label": "Övre etikett",
|
||||||
"url_filters": "URL-filter",
|
"url_filters": "URL-filter",
|
||||||
"url_not_supported": "URL stöds inte",
|
"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_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_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.",
|
"variable_name_is_already_taken_please_choose_another": "Variabelnamnet är redan taget, vänligen välj ett annat.",
|
||||||
|
|||||||
@@ -238,7 +238,6 @@
|
|||||||
"imprint": "印记",
|
"imprint": "印记",
|
||||||
"in_progress": "进行中",
|
"in_progress": "进行中",
|
||||||
"inactive_surveys": "不 活跃 调查",
|
"inactive_surveys": "不 活跃 调查",
|
||||||
"input_type": "输入类型",
|
|
||||||
"integration": "集成",
|
"integration": "集成",
|
||||||
"integrations": "集成",
|
"integrations": "集成",
|
||||||
"invalid_date": "无效 日期",
|
"invalid_date": "无效 日期",
|
||||||
@@ -262,13 +261,11 @@
|
|||||||
"look_and_feel": "外观 & 感觉",
|
"look_and_feel": "外观 & 感觉",
|
||||||
"manage": "管理",
|
"manage": "管理",
|
||||||
"marketing": "市场营销",
|
"marketing": "市场营销",
|
||||||
"maximum": "最大值",
|
|
||||||
"member": "成员",
|
"member": "成员",
|
||||||
"members": "成员",
|
"members": "成员",
|
||||||
"members_and_teams": "成员和团队",
|
"members_and_teams": "成员和团队",
|
||||||
"membership_not_found": "未找到会员资格",
|
"membership_not_found": "未找到会员资格",
|
||||||
"metadata": "元数据",
|
"metadata": "元数据",
|
||||||
"minimum": "最低",
|
|
||||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks 在 更大 的 屏幕 上 效果 最佳。 若 需要 管理 或 构建 调查, 请 切换 到 其他 设备。",
|
"mobile_overlay_app_works_best_on_desktop": "Formbricks 在 更大 的 屏幕 上 效果 最佳。 若 需要 管理 或 构建 调查, 请 切换 到 其他 设备。",
|
||||||
"mobile_overlay_surveys_look_good": "别 担心 – 您 的 调查 在 每 一 种 设备 和 屏幕 尺寸 上 看起来 都 很 棒!",
|
"mobile_overlay_surveys_look_good": "别 担心 – 您 的 调查 在 每 一 种 设备 和 屏幕 尺寸 上 看起来 都 很 棒!",
|
||||||
"mobile_overlay_title": "噢, 检测 到 小 屏幕!",
|
"mobile_overlay_title": "噢, 检测 到 小 屏幕!",
|
||||||
@@ -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_survey_type_will_remove_existing_distribution_channels": "更改 调查 类型 会影 响 分享 方式 。 如果 受访者 已经 拥有 当前 类型 的 访问 链接 , 在 更改 之后 ,他们 可能 会 失去 访问 权限 。",
|
||||||
"character_limit_toggle_description": "限制 答案的短或长程度。",
|
|
||||||
"character_limit_toggle_title": "添加 字符限制",
|
|
||||||
"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 过滤器",
|
"url_filters": "URL 过滤器",
|
||||||
"url_not_supported": "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_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_is_used_in_quota_please_remove_it_from_quota_first": "变量 \"{variableName}\" 正在 被 \"{quotaName}\" 配额 使用",
|
||||||
"variable_name_is_already_taken_please_choose_another": "变量名已被占用,请选择其他。",
|
"variable_name_is_already_taken_please_choose_another": "变量名已被占用,请选择其他。",
|
||||||
|
|||||||
@@ -238,7 +238,6 @@
|
|||||||
"imprint": "版本訊息",
|
"imprint": "版本訊息",
|
||||||
"in_progress": "進行中",
|
"in_progress": "進行中",
|
||||||
"inactive_surveys": "停用中的問卷",
|
"inactive_surveys": "停用中的問卷",
|
||||||
"input_type": "輸入類型",
|
|
||||||
"integration": "整合",
|
"integration": "整合",
|
||||||
"integrations": "整合",
|
"integrations": "整合",
|
||||||
"invalid_date": "無效日期",
|
"invalid_date": "無效日期",
|
||||||
@@ -262,13 +261,11 @@
|
|||||||
"look_and_feel": "外觀與風格",
|
"look_and_feel": "外觀與風格",
|
||||||
"manage": "管理",
|
"manage": "管理",
|
||||||
"marketing": "行銷",
|
"marketing": "行銷",
|
||||||
"maximum": "最大值",
|
|
||||||
"member": "成員",
|
"member": "成員",
|
||||||
"members": "成員",
|
"members": "成員",
|
||||||
"members_and_teams": "成員與團隊",
|
"members_and_teams": "成員與團隊",
|
||||||
"membership_not_found": "找不到成員資格",
|
"membership_not_found": "找不到成員資格",
|
||||||
"metadata": "元數據",
|
"metadata": "元數據",
|
||||||
"minimum": "最小值",
|
|
||||||
"mobile_overlay_app_works_best_on_desktop": "Formbricks 適合在大螢幕上使用。若要管理或建立問卷,請切換到其他裝置。",
|
"mobile_overlay_app_works_best_on_desktop": "Formbricks 適合在大螢幕上使用。若要管理或建立問卷,請切換到其他裝置。",
|
||||||
"mobile_overlay_surveys_look_good": "別擔心 -你的 問卷 在每個 裝置 和 螢幕尺寸 上 都 很出色!",
|
"mobile_overlay_surveys_look_good": "別擔心 -你的 問卷 在每個 裝置 和 螢幕尺寸 上 都 很出色!",
|
||||||
"mobile_overlay_title": "糟糕 ,偵測到小螢幕!",
|
"mobile_overlay_title": "糟糕 ,偵測到小螢幕!",
|
||||||
@@ -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_survey_type_will_remove_existing_distribution_channels": "更改問卷類型會影響其共享方式。如果受訪者已擁有當前類型的存取連結,則在切換後可能會失去存取權限。",
|
||||||
"character_limit_toggle_description": "限制答案的長度或短度。",
|
|
||||||
"character_limit_toggle_title": "新增字元限制",
|
|
||||||
"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": "個字元",
|
||||||
|
"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_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_is_used_in_quota_please_remove_it_from_quota_first": "變數 \"{variableName}\" 正被使用於 \"{quotaName}\" 配額中",
|
||||||
"variable_name_is_already_taken_please_choose_another": "已使用此變數名稱,請選擇另一個名稱。",
|
"variable_name_is_already_taken_please_choose_another": "已使用此變數名稱,請選擇另一個名稱。",
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { type TI18nString } from "@formbricks/types/i18n";
|
|||||||
import {
|
import {
|
||||||
TSurveyElement,
|
TSurveyElement,
|
||||||
TSurveyElementChoice,
|
TSurveyElementChoice,
|
||||||
TSurveyElementTypeEnum,
|
|
||||||
} from "@formbricks/types/surveys/elements";
|
} from "@formbricks/types/surveys/elements";
|
||||||
import { TSurvey, TSurveyEndScreenCard, TSurveyRedirectUrlCard } from "@formbricks/types/surveys/types";
|
import { TSurvey, TSurveyEndScreenCard, TSurveyRedirectUrlCard } from "@formbricks/types/surveys/types";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
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 { FileInput } from "@/modules/ui/components/file-input";
|
||||||
import { Input } from "@/modules/ui/components/input";
|
import { Input } from "@/modules/ui/components/input";
|
||||||
import { Label } from "@/modules/ui/components/label";
|
import { Label } from "@/modules/ui/components/label";
|
||||||
import { Switch } from "@/modules/ui/components/switch";
|
|
||||||
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||||
import {
|
import {
|
||||||
determineImageUploaderVisibility,
|
determineImageUploaderVisibility,
|
||||||
@@ -315,70 +313,6 @@ export const ElementFormInput = ({
|
|||||||
return false;
|
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";
|
const useRichTextEditor = id === "headline" || id === "subheader" || id === "html";
|
||||||
|
|
||||||
@@ -393,21 +327,6 @@ export const ElementFormInput = ({
|
|||||||
{label && (
|
{label && (
|
||||||
<div className="mb-2 mt-3 flex items-center justify-between">
|
<div className="mb-2 mt-3 flex items-center justify-between">
|
||||||
<Label htmlFor={id}>{label}</Label>
|
<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>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-4" ref={animationParent}>
|
<div className="flex flex-col gap-4" ref={animationParent}>
|
||||||
@@ -523,21 +442,6 @@ export const ElementFormInput = ({
|
|||||||
{label && (
|
{label && (
|
||||||
<div className="mb-2 mt-3 flex items-center justify-between">
|
<div className="mb-2 mt-3 flex items-center justify-between">
|
||||||
<Label htmlFor={id}>{label}</Label>
|
<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>
|
||||||
)}
|
)}
|
||||||
<MultiLangWrapper
|
<MultiLangWrapper
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
|||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { type JSX, useEffect } from "react";
|
import { type JSX, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
import { TValidationRulesForAddress } from "@formbricks/types/surveys/validation-rules";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
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 { Button } from "@/modules/ui/components/button";
|
||||||
import { ElementToggleTable } from "@/modules/ui/components/element-toggle-table";
|
import { ElementToggleTable } from "@/modules/ui/components/element-toggle-table";
|
||||||
|
|
||||||
@@ -159,6 +161,16 @@ export const AddressElementForm = ({
|
|||||||
isStorageConfigured={isStorageConfigured}
|
isStorageConfigured={isStorageConfigured}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ValidationRulesEditor
|
||||||
|
elementType={TSurveyElementTypeEnum.Address}
|
||||||
|
validationRules={element.validationRules ?? []}
|
||||||
|
onUpdateRules={(rules: TValidationRulesForAddress) => {
|
||||||
|
updateElement(elementIdx, {
|
||||||
|
validationRules: rules,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { type JSX, useEffect, useState } from "react";
|
import { type JSX, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
import { TValidationRulesForCal } from "@formbricks/types/surveys/validation-rules";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
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 { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Input } from "@/modules/ui/components/input";
|
import { Input } from "@/modules/ui/components/input";
|
||||||
@@ -143,6 +145,16 @@ export const CalElementForm = ({
|
|||||||
</AdvancedOptionToggle>
|
</AdvancedOptionToggle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ValidationRulesEditor
|
||||||
|
elementType={TSurveyElementTypeEnum.Cal}
|
||||||
|
validationRules={element.validationRules ?? []}
|
||||||
|
onUpdateRules={(rules: TValidationRulesForCal) => {
|
||||||
|
updateElement(elementIdx, {
|
||||||
|
validationRules: rules,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
|||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { type JSX } from "react";
|
import { type JSX } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
import { TValidationRulesForConsent } from "@formbricks/types/surveys/validation-rules";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
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 { Button } from "@/modules/ui/components/button";
|
||||||
|
|
||||||
interface ConsentElementFormProps {
|
interface ConsentElementFormProps {
|
||||||
@@ -102,6 +104,16 @@ export const ConsentElementForm = ({
|
|||||||
placeholder="I agree to the terms and conditions"
|
placeholder="I agree to the terms and conditions"
|
||||||
value={element.label}
|
value={element.label}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ValidationRulesEditor
|
||||||
|
elementType={TSurveyElementTypeEnum.Consent}
|
||||||
|
validationRules={element.validationRules ?? []}
|
||||||
|
onUpdateRules={(rules: TValidationRulesForConsent) => {
|
||||||
|
updateElement(elementIdx, {
|
||||||
|
validationRules: rules,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
|||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { type JSX, useEffect } from "react";
|
import { type JSX, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
import { TValidationRulesForContactInfo } from "@formbricks/types/surveys/validation-rules";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
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 { Button } from "@/modules/ui/components/button";
|
||||||
import { ElementToggleTable } from "@/modules/ui/components/element-toggle-table";
|
import { ElementToggleTable } from "@/modules/ui/components/element-toggle-table";
|
||||||
|
|
||||||
@@ -156,6 +158,16 @@ export const ContactInfoElementForm = ({
|
|||||||
isStorageConfigured={isStorageConfigured}
|
isStorageConfigured={isStorageConfigured}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ValidationRulesEditor
|
||||||
|
elementType={TSurveyElementTypeEnum.ContactInfo}
|
||||||
|
validationRules={element.validationRules ?? []}
|
||||||
|
onUpdateRules={(rules: TValidationRulesForContactInfo) => {
|
||||||
|
updateElement(elementIdx, {
|
||||||
|
validationRules: rules,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
|||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { type JSX } from "react";
|
import { type JSX } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
import { TValidationRulesForDate } from "@formbricks/types/surveys/validation-rules";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
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 { Button } from "@/modules/ui/components/button";
|
||||||
import { Label } from "@/modules/ui/components/label";
|
import { Label } from "@/modules/ui/components/label";
|
||||||
import { OptionsSwitch } from "@/modules/ui/components/options-switch";
|
import { OptionsSwitch } from "@/modules/ui/components/options-switch";
|
||||||
@@ -126,6 +128,16 @@ export const DateElementForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ValidationRulesEditor
|
||||||
|
elementType={TSurveyElementTypeEnum.Date}
|
||||||
|
validationRules={element.validationRules ?? []}
|
||||||
|
onUpdateRules={(rules: TValidationRulesForDate) => {
|
||||||
|
updateElement(elementIdx, {
|
||||||
|
validationRules: rules,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import { type JSX, useMemo, useState } from "react";
|
|||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TAllowedFileExtension, ZAllowedFileExtension } from "@formbricks/types/storage";
|
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 { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
import { TValidationRulesForFileUpload } from "@formbricks/types/surveys/validation-rules";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
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 { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Input } from "@/modules/ui/components/input";
|
import { Input } from "@/modules/ui/components/input";
|
||||||
@@ -229,7 +231,7 @@ export const FileUploadElementForm = ({
|
|||||||
|
|
||||||
updateElement(elementIdx, { maxSizeInMB: parseInt(e.target.value, 10) });
|
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
|
MB
|
||||||
</p>
|
</p>
|
||||||
@@ -290,6 +292,16 @@ export const FileUploadElementForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</AdvancedOptionToggle>
|
</AdvancedOptionToggle>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ValidationRulesEditor
|
||||||
|
elementType={TSurveyElementTypeEnum.FileUpload}
|
||||||
|
validationRules={element.validationRules ?? []}
|
||||||
|
onUpdateRules={(rules: TValidationRulesForFileUpload) => {
|
||||||
|
updateElement(elementIdx, {
|
||||||
|
validationRules: rules,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import { type JSX, useCallback } from "react";
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TI18nString } from "@formbricks/types/i18n";
|
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 { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
import { TValidationRulesForMatrix } from "@formbricks/types/surveys/validation-rules";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||||
import { MatrixSortableItem } from "@/modules/survey/editor/components/matrix-sortable-item";
|
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 { findOptionUsedInLogic } from "@/modules/survey/editor/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Label } from "@/modules/ui/components/label";
|
import { Label } from "@/modules/ui/components/label";
|
||||||
@@ -347,6 +349,16 @@ export const MatrixElementForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ValidationRulesEditor
|
||||||
|
elementType={TSurveyElementTypeEnum.Matrix}
|
||||||
|
validationRules={element.validationRules ?? []}
|
||||||
|
onUpdateRules={(rules: TValidationRulesForMatrix) => {
|
||||||
|
updateElement(elementIdx, {
|
||||||
|
validationRules: rules,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,11 +12,16 @@ import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
|
|||||||
import { TI18nString } from "@formbricks/types/i18n";
|
import { TI18nString } from "@formbricks/types/i18n";
|
||||||
import { TSurveyElementTypeEnum, TSurveyMultipleChoiceElement } from "@formbricks/types/surveys/elements";
|
import { TSurveyElementTypeEnum, TSurveyMultipleChoiceElement } from "@formbricks/types/surveys/elements";
|
||||||
import { TShuffleOption, TSurvey } from "@formbricks/types/surveys/types";
|
import { TShuffleOption, TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
import {
|
||||||
|
TValidationRulesForMultipleChoiceMulti,
|
||||||
|
TValidationRulesForMultipleChoiceSingle,
|
||||||
|
} from "@formbricks/types/surveys/validation-rules";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||||
import { BulkEditOptionsModal } from "@/modules/survey/editor/components/bulk-edit-options-modal";
|
import { BulkEditOptionsModal } from "@/modules/survey/editor/components/bulk-edit-options-modal";
|
||||||
import { ElementOptionChoice } from "@/modules/survey/editor/components/element-option-choice";
|
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 { findOptionUsedInLogic } from "@/modules/survey/editor/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Label } from "@/modules/ui/components/label";
|
import { Label } from "@/modules/ui/components/label";
|
||||||
@@ -398,6 +403,28 @@ export const MultipleChoiceElementForm = ({
|
|||||||
surveyLanguageCodes={surveyLanguageCodes}
|
surveyLanguageCodes={surveyLanguageCodes}
|
||||||
locale={locale}
|
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>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
|||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { type JSX } from "react";
|
import { type JSX } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
import { TValidationRulesForNPS } from "@formbricks/types/surveys/validation-rules";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
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 { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
|
|
||||||
@@ -140,6 +142,16 @@ export const NPSElementForm = ({
|
|||||||
childBorder
|
childBorder
|
||||||
customContainerClass="p-0 mt-4"
|
customContainerClass="p-0 mt-4"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ValidationRulesEditor
|
||||||
|
elementType={TSurveyElementTypeEnum.NPS}
|
||||||
|
validationRules={element.validationRules ?? []}
|
||||||
|
onUpdateRules={(rules: TValidationRulesForNPS) => {
|
||||||
|
updateElement(elementIdx, {
|
||||||
|
validationRules: rules,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { HashIcon, LinkIcon, MailIcon, MessageSquareTextIcon, PhoneIcon, PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { JSX, useEffect, useState } from "react";
|
import { JSX } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
import { TValidationRulesForOpenText } from "@formbricks/types/surveys/validation-rules";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
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 { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
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 {
|
interface OpenElementFormProps {
|
||||||
localSurvey: TSurvey;
|
localSurvey: TSurvey;
|
||||||
@@ -42,43 +45,10 @@ export const OpenElementForm = ({
|
|||||||
isExternalUrlsAllowed,
|
isExternalUrlsAllowed,
|
||||||
}: OpenElementFormProps): JSX.Element => {
|
}: OpenElementFormProps): JSX.Element => {
|
||||||
const { t } = useTranslation();
|
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 defaultPlaceholder = getPlaceholderByInputType(element.inputType ?? "text");
|
||||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
|
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 [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 (
|
return (
|
||||||
<form>
|
<form>
|
||||||
@@ -156,80 +126,7 @@ export const OpenElementForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<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">
|
<div className="mt-4">
|
||||||
<AdvancedOptionToggle
|
<AdvancedOptionToggle
|
||||||
isChecked={element.longAnswer !== false}
|
isChecked={element.longAnswer !== false}
|
||||||
@@ -245,6 +142,16 @@ export const OpenElementForm = ({
|
|||||||
customContainerClass="p-0"
|
customContainerClass="p-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ValidationRulesEditor
|
||||||
|
elementType={TSurveyElementTypeEnum.OpenText}
|
||||||
|
validationRules={element.validationRules ?? []}
|
||||||
|
onUpdateRules={(rules: TValidationRulesForOpenText) => {
|
||||||
|
updateElement(elementIdx, {
|
||||||
|
validationRules: rules,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import { createId } from "@paralleldrive/cuid2";
|
|||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { type JSX } from "react";
|
import { type JSX } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
import { TValidationRulesForPictureSelection } from "@formbricks/types/surveys/validation-rules";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
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 { Button } from "@/modules/ui/components/button";
|
||||||
import { FileInput } from "@/modules/ui/components/file-input";
|
import { FileInput } from "@/modules/ui/components/file-input";
|
||||||
import { Label } from "@/modules/ui/components/label";
|
import { Label } from "@/modules/ui/components/label";
|
||||||
@@ -170,6 +172,16 @@ export const PictureSelectionForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ValidationRulesEditor
|
||||||
|
elementType={TSurveyElementTypeEnum.PictureSelection}
|
||||||
|
validationRules={element.validationRules ?? []}
|
||||||
|
onUpdateRules={(rules: TValidationRulesForPictureSelection) => {
|
||||||
|
updateElement(elementIdx, {
|
||||||
|
validationRules: rules,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import { PlusIcon } from "lucide-react";
|
|||||||
import { type JSX, useEffect, useRef, useState } from "react";
|
import { type JSX, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TI18nString } from "@formbricks/types/i18n";
|
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 { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
import { TValidationRulesForRanking } from "@formbricks/types/surveys/validation-rules";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||||
import { ElementOptionChoice } from "@/modules/survey/editor/components/element-option-choice";
|
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 { Button } from "@/modules/ui/components/button";
|
||||||
import { Label } from "@/modules/ui/components/label";
|
import { Label } from "@/modules/ui/components/label";
|
||||||
import { ShuffleOptionSelect } from "@/modules/ui/components/shuffle-option-select";
|
import { ShuffleOptionSelect } from "@/modules/ui/components/shuffle-option-select";
|
||||||
@@ -246,6 +248,16 @@ export const RankingElementForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ValidationRulesEditor
|
||||||
|
elementType={TSurveyElementTypeEnum.Ranking}
|
||||||
|
validationRules={element.validationRules ?? []}
|
||||||
|
onUpdateRules={(rules: TValidationRulesForRanking) => {
|
||||||
|
updateElement(elementIdx, {
|
||||||
|
validationRules: rules,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { HashIcon, PlusIcon, SmileIcon, StarIcon } from "lucide-react";
|
import { HashIcon, PlusIcon, SmileIcon, StarIcon } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
import { TValidationRulesForRating } from "@formbricks/types/surveys/validation-rules";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||||
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
import { ElementFormInput } from "@/modules/survey/components/element-form-input";
|
||||||
import { Dropdown } from "@/modules/survey/editor/components/rating-type-dropdown";
|
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 { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Label } from "@/modules/ui/components/label";
|
import { Label } from "@/modules/ui/components/label";
|
||||||
@@ -189,6 +191,16 @@ export const RatingElementForm = ({
|
|||||||
customContainerClass="p-0 mt-4"
|
customContainerClass="p-0 mt-4"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<ValidationRulesEditor
|
||||||
|
elementType={TSurveyElementTypeEnum.Rating}
|
||||||
|
validationRules={element.validationRules ?? []}
|
||||||
|
onUpdateRules={(rules: TValidationRulesForRating) => {
|
||||||
|
updateElement(elementIdx, {
|
||||||
|
validationRules: rules,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</form>
|
</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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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>) {
|
}: Readonly<AddressElementProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isCurrent = element.id === currentElementId;
|
const isCurrent = element.id === currentElementId;
|
||||||
|
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||||
|
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||||
|
|
||||||
@@ -117,7 +118,7 @@ export function AddressElement({
|
|||||||
fields={formFields}
|
fields={formFields}
|
||||||
value={convertToValueObject(value)}
|
value={convertToValueObject(value)}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={element.required}
|
required={isRequired}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
videoUrl={element.videoUrl}
|
videoUrl={element.videoUrl}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export function CalElement({
|
|||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
const isMediaAvailable = element.imageUrl || element.videoUrl;
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, element.id === currentElementId);
|
||||||
|
|
||||||
const onSuccessfulBooking = useCallback(() => {
|
const onSuccessfulBooking = useCallback(() => {
|
||||||
@@ -46,7 +47,7 @@ export function CalElement({
|
|||||||
key={element.id}
|
key={element.id}
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (element.required && !value) {
|
if (isRequired && !value) {
|
||||||
setErrorMessage(t("errors.please_book_an_appointment"));
|
setErrorMessage(t("errors.please_book_an_appointment"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -62,7 +63,7 @@ export function CalElement({
|
|||||||
<Headline
|
<Headline
|
||||||
headline={getLocalizedValue(element.headline, languageCode)}
|
headline={getLocalizedValue(element.headline, languageCode)}
|
||||||
elementId={element.id}
|
elementId={element.id}
|
||||||
required={element.required}
|
validationRules={element.validationRules}
|
||||||
/>
|
/>
|
||||||
<Subheader
|
<Subheader
|
||||||
subheader={element.subheader ? getLocalizedValue(element.subheader, languageCode) : ""}
|
subheader={element.subheader ? getLocalizedValue(element.subheader, languageCode) : ""}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export function ConsentElement({
|
|||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||||
const isCurrent = element.id === currentElementId;
|
const isCurrent = element.id === currentElementId;
|
||||||
|
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ export function ConsentElement({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const validateRequired = (): boolean => {
|
const validateRequired = (): boolean => {
|
||||||
if (element.required && value !== "accepted") {
|
if (isRequired && value !== "accepted") {
|
||||||
setErrorMessage(t("errors.please_fill_out_this_field"));
|
setErrorMessage(t("errors.please_fill_out_this_field"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -65,7 +66,7 @@ export function ConsentElement({
|
|||||||
checkboxLabel={getLocalizedValue(element.label, languageCode)}
|
checkboxLabel={getLocalizedValue(element.label, languageCode)}
|
||||||
value={value === "accepted"}
|
value={value === "accepted"}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={element.required}
|
required={isRequired}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function ContactInfoElement({
|
|||||||
}: Readonly<ContactInfoElementProps>) {
|
}: Readonly<ContactInfoElementProps>) {
|
||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isCurrent = element.id === currentElementId;
|
const isCurrent = element.id === currentElementId;
|
||||||
|
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||||
|
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||||
|
|
||||||
@@ -113,7 +114,7 @@ export function ContactInfoElement({
|
|||||||
fields={formFields}
|
fields={formFields}
|
||||||
value={convertToValueObject(value)}
|
value={convertToValueObject(value)}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={element.required}
|
required={isRequired}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
videoUrl={element.videoUrl}
|
videoUrl={element.videoUrl}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function DateElement({
|
|||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||||
const isCurrent = element.id === currentElementId;
|
const isCurrent = element.id === currentElementId;
|
||||||
|
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||||
|
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -41,7 +42,7 @@ export function DateElement({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const validateRequired = (): boolean => {
|
const validateRequired = (): boolean => {
|
||||||
if (element.required && (!value || value.trim() === "")) {
|
if (isRequired && (!value || value.trim() === "")) {
|
||||||
setErrorMessage(t("errors.please_select_a_date"));
|
setErrorMessage(t("errors.please_select_a_date"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -75,7 +76,7 @@ export function DateElement({
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
minDate={getMinDate()}
|
minDate={getMinDate()}
|
||||||
maxDate={getMaxDate()}
|
maxDate={getMaxDate()}
|
||||||
required={element.required}
|
required={isRequired}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
locale={languageCode}
|
locale={languageCode}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export function FileUploadElement({
|
|||||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const isCurrent = element.id === currentElementId;
|
const isCurrent = element.id === currentElementId;
|
||||||
|
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||||
|
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -326,7 +327,7 @@ export function FileUploadElement({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const validateRequired = (): boolean => {
|
const validateRequired = (): boolean => {
|
||||||
if (element.required && (!value || value.length === 0)) {
|
if (isRequired && (!value || value.length === 0)) {
|
||||||
setErrorMessage(t("errors.please_upload_a_file"));
|
setErrorMessage(t("errors.please_upload_a_file"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -353,7 +354,7 @@ export function FileUploadElement({
|
|||||||
onFileSelect={handleFileSelect}
|
onFileSelect={handleFileSelect}
|
||||||
allowMultiple={element.allowMultipleFiles}
|
allowMultiple={element.allowMultipleFiles}
|
||||||
allowedFileExtensions={element.allowedFileExtensions}
|
allowedFileExtensions={element.allowedFileExtensions}
|
||||||
required={element.required}
|
required={isRequired}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
isUploading={isUploading}
|
isUploading={isUploading}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export function MatrixElement({
|
|||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||||
const isCurrent = element.id === currentElementId;
|
const isCurrent = element.id === currentElementId;
|
||||||
|
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||||
|
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -121,7 +122,7 @@ export function MatrixElement({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const validateRequired = (): boolean => {
|
const validateRequired = (): boolean => {
|
||||||
if (element.required) {
|
if (isRequired) {
|
||||||
const hasUnansweredRows = rows.some((row) => !value[row.label]);
|
const hasUnansweredRows = rows.some((row) => !value[row.label]);
|
||||||
if (hasUnansweredRows) {
|
if (hasUnansweredRows) {
|
||||||
setErrorMessage(t("errors.please_select_an_option"));
|
setErrorMessage(t("errors.please_select_an_option"));
|
||||||
@@ -150,7 +151,7 @@ export function MatrixElement({
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
value={convertValueToIds(value)}
|
value={convertValueToIds(value)}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={element.required}
|
required={isRequired}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
videoUrl={element.videoUrl}
|
videoUrl={element.videoUrl}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export function MultipleChoiceMultiElement({
|
|||||||
const [otherValue, setOtherValue] = useState("");
|
const [otherValue, setOtherValue] = useState("");
|
||||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||||
const isCurrent = element.id === currentElementId;
|
const isCurrent = element.id === currentElementId;
|
||||||
|
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||||
|
|
||||||
@@ -174,11 +175,11 @@ export function MultipleChoiceMultiElement({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const validateRequired = (): boolean => {
|
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"));
|
setErrorMessage(t("errors.please_select_an_option"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (element.required && isOtherSelected && !otherValue.trim()) {
|
if (isRequired && isOtherSelected && !otherValue.trim()) {
|
||||||
setErrorMessage(t("errors.please_fill_out_this_field"));
|
setErrorMessage(t("errors.please_fill_out_this_field"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -262,7 +263,7 @@ export function MultipleChoiceMultiElement({
|
|||||||
options={allOptions}
|
options={allOptions}
|
||||||
value={selectedValues}
|
value={selectedValues}
|
||||||
onChange={handleMultiSelectChange}
|
onChange={handleMultiSelectChange}
|
||||||
required={element.required}
|
required={isRequired}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
otherOptionId={otherOption?.id}
|
otherOptionId={otherOption?.id}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export function MultipleChoiceSingleElement({
|
|||||||
const [otherValue, setOtherValue] = useState("");
|
const [otherValue, setOtherValue] = useState("");
|
||||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||||
const isCurrent = element.id === currentElementId;
|
const isCurrent = element.id === currentElementId;
|
||||||
|
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -154,12 +155,12 @@ export function MultipleChoiceSingleElement({
|
|||||||
|
|
||||||
const validateRequired = (): boolean => {
|
const validateRequired = (): boolean => {
|
||||||
// Check if nothing is selected
|
// Check if nothing is selected
|
||||||
if (element.required && selectedValue === undefined) {
|
if (isRequired && selectedValue === undefined) {
|
||||||
setErrorMessage(t("errors.please_select_an_option"));
|
setErrorMessage(t("errors.please_select_an_option"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Check if "other" is selected but not filled
|
// 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"));
|
setErrorMessage(t("errors.please_fill_out_this_field"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -184,7 +185,7 @@ export function MultipleChoiceSingleElement({
|
|||||||
options={allOptions}
|
options={allOptions}
|
||||||
value={selectedValue}
|
value={selectedValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={element.required}
|
required={isRequired}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
otherOptionId={otherOption?.id}
|
otherOptionId={otherOption?.id}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export function NPSElement({
|
|||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||||
const isCurrent = element.id === currentElementId;
|
const isCurrent = element.id === currentElementId;
|
||||||
|
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ export function NPSElement({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const validateRequired = (): boolean => {
|
const validateRequired = (): boolean => {
|
||||||
if (element.required && value === undefined) {
|
if (isRequired && value === undefined) {
|
||||||
setErrorMessage(t("errors.please_select_an_option"));
|
setErrorMessage(t("errors.please_select_an_option"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -70,7 +71,7 @@ export function NPSElement({
|
|||||||
lowerLabel={getLocalizedValue(element.lowerLabel, languageCode)}
|
lowerLabel={getLocalizedValue(element.lowerLabel, languageCode)}
|
||||||
upperLabel={getLocalizedValue(element.upperLabel, languageCode)}
|
upperLabel={getLocalizedValue(element.upperLabel, languageCode)}
|
||||||
colorCoding={element.isColorCodingEnabled}
|
colorCoding={element.isColorCodingEnabled}
|
||||||
required={element.required}
|
required={isRequired}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export function OpenTextElement({
|
|||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||||
const isCurrent = element.id === currentElementId;
|
const isCurrent = element.id === currentElementId;
|
||||||
|
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ export function OpenTextElement({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const validateRequired = (): boolean => {
|
const validateRequired = (): boolean => {
|
||||||
if (element.required && (!value || value.trim() === "")) {
|
if (isRequired && (!value || value.trim() === "")) {
|
||||||
setErrorMessage(t("errors.please_fill_out_this_field"));
|
setErrorMessage(t("errors.please_fill_out_this_field"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -122,7 +123,7 @@ export function OpenTextElement({
|
|||||||
placeholder={getLocalizedValue(element.placeholder, languageCode)}
|
placeholder={getLocalizedValue(element.placeholder, languageCode)}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={element.required}
|
required={isRequired}
|
||||||
longAnswer={element.longAnswer !== false}
|
longAnswer={element.longAnswer !== false}
|
||||||
inputType={getInputType()}
|
inputType={getInputType()}
|
||||||
charLimit={element.inputType === "text" ? element.charLimit : undefined}
|
charLimit={element.inputType === "text" ? element.charLimit : undefined}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export function PictureSelectionElement({
|
|||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isCurrent = element.id === currentElementId;
|
const isCurrent = element.id === currentElementId;
|
||||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
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);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// Convert choices to PictureSelectOption format
|
// Convert choices to PictureSelectOption format
|
||||||
@@ -66,7 +67,7 @@ export function PictureSelectionElement({
|
|||||||
|
|
||||||
const handleSubmit = (e: Event) => {
|
const handleSubmit = (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (element.required) {
|
if (isRequired) {
|
||||||
if (element.allowMulti) {
|
if (element.allowMulti) {
|
||||||
if (!currentValue || !Array.isArray(currentValue) || currentValue.length === 0) {
|
if (!currentValue || !Array.isArray(currentValue) || currentValue.length === 0) {
|
||||||
setErrorMessage(t("errors.please_select_an_option"));
|
setErrorMessage(t("errors.please_select_an_option"));
|
||||||
@@ -94,7 +95,7 @@ export function PictureSelectionElement({
|
|||||||
value={currentValue}
|
value={currentValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
allowMulti={element.allowMulti}
|
allowMulti={element.allowMulti}
|
||||||
required={element.required}
|
required={isRequired}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export function RankingElement({
|
|||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||||
const isCurrent = element.id === currentElementId;
|
const isCurrent = element.id === currentElementId;
|
||||||
|
const isRequired = element.validationRules?.some((rule) => rule.type === "required") ?? false;
|
||||||
|
|
||||||
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||||
|
|
||||||
@@ -109,7 +110,7 @@ export function RankingElement({
|
|||||||
const isValueArray = Array.isArray(value);
|
const isValueArray = Array.isArray(value);
|
||||||
const allItemsRanked = isValueArray && value.length === element.choices.length;
|
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"));
|
setErrorMessage(t("errors.please_rank_all_items_before_submitting"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -135,7 +136,7 @@ export function RankingElement({
|
|||||||
options={options}
|
options={options}
|
||||||
value={selectedValues}
|
value={selectedValues}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
required={element.required}
|
required={isRequired}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
videoUrl={element.videoUrl}
|
videoUrl={element.videoUrl}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function RatingElement({
|
|||||||
const [startTime, setStartTime] = useState(performance.now());
|
const [startTime, setStartTime] = useState(performance.now());
|
||||||
const isCurrent = element.id === currentElementId;
|
const isCurrent = element.id === currentElementId;
|
||||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
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);
|
useTtc(element.id, ttc, setTtc, startTime, setStartTime, isCurrent);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ export function RatingElement({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const validateRequired = (): boolean => {
|
const validateRequired = (): boolean => {
|
||||||
if (element.required && !value) {
|
if (isRequired && !value) {
|
||||||
setErrorMessage(t("errors.please_select_an_option"));
|
setErrorMessage(t("errors.please_select_an_option"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -69,7 +70,7 @@ export function RatingElement({
|
|||||||
lowerLabel={getLocalizedValue(element.lowerLabel, languageCode)}
|
lowerLabel={getLocalizedValue(element.lowerLabel, languageCode)}
|
||||||
upperLabel={getLocalizedValue(element.upperLabel, languageCode)}
|
upperLabel={getLocalizedValue(element.upperLabel, languageCode)}
|
||||||
colorCoding={element.isColorCodingEnabled}
|
colorCoding={element.isColorCodingEnabled}
|
||||||
required={element.required}
|
required={isRequired}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
imageUrl={element.imageUrl}
|
imageUrl={element.imageUrl}
|
||||||
videoUrl={element.videoUrl}
|
videoUrl={element.videoUrl}
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import DOMPurify from "isomorphic-dompurify";
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { TValidationRule } from "@formbricks/types/surveys/validation-rules";
|
||||||
import { isValidHTML, stripInlineStyles } from "@/lib/html-utils";
|
import { isValidHTML, stripInlineStyles } from "@/lib/html-utils";
|
||||||
|
|
||||||
interface HeadlineProps {
|
interface HeadlineProps {
|
||||||
headline: string;
|
headline: string;
|
||||||
elementId: string;
|
elementId: string;
|
||||||
required?: boolean;
|
validationRules?: TValidationRule[];
|
||||||
alignTextCenter?: boolean;
|
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 { t } = useTranslation();
|
||||||
const isQuestionCard = elementId !== "EndingCard" && elementId !== "welcomeCard";
|
const isQuestionCard = elementId !== "EndingCard" && elementId !== "welcomeCard";
|
||||||
// Strip inline styles BEFORE parsing to avoid CSP violations
|
// Strip inline styles BEFORE parsing to avoid CSP violations
|
||||||
@@ -18,14 +25,14 @@ export function Headline({ headline, elementId, required = true, alignTextCenter
|
|||||||
const safeHtml =
|
const safeHtml =
|
||||||
isHeadlineHtml && strippedHeadline
|
isHeadlineHtml && strippedHeadline
|
||||||
? DOMPurify.sanitize(strippedHeadline, {
|
? DOMPurify.sanitize(strippedHeadline, {
|
||||||
ADD_ATTR: ["target"],
|
ADD_ATTR: ["target"],
|
||||||
FORBID_ATTR: ["style"], // Additional safeguard to remove any remaining inline styles
|
FORBID_ATTR: ["style"], // Additional safeguard to remove any remaining inline styles
|
||||||
})
|
})
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label htmlFor={elementId} className="text-heading mb-[3px] flex flex-col">
|
<label htmlFor={elementId} className="text-heading mb-[3px] flex flex-col">
|
||||||
{required && isQuestionCard && (
|
{hasRequiredRule && isQuestionCard && (
|
||||||
<span
|
<span
|
||||||
className="mb-[3px] text-xs leading-6 font-normal opacity-60"
|
className="mb-[3px] text-xs leading-6 font-normal opacity-60"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
|||||||
@@ -3,6 +3,22 @@ import { ZUrl } from "../common";
|
|||||||
import { ZI18nString } from "../i18n";
|
import { ZI18nString } from "../i18n";
|
||||||
import { ZAllowedFileExtension } from "../storage";
|
import { ZAllowedFileExtension } from "../storage";
|
||||||
import { FORBIDDEN_IDS } from "./validation";
|
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)
|
// Element Type Enum (same as question types)
|
||||||
export enum TSurveyElementTypeEnum {
|
export enum TSurveyElementTypeEnum {
|
||||||
@@ -50,6 +66,7 @@ export const ZSurveyElementId = z.string().superRefine((id, ctx) => {
|
|||||||
export type TSurveyElementId = z.infer<typeof ZSurveyElementId>;
|
export type TSurveyElementId = z.infer<typeof ZSurveyElementId>;
|
||||||
|
|
||||||
// Base element (like ZSurveyQuestionBase but WITHOUT logic, buttonLabel, backButtonLabel)
|
// 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({
|
export const ZSurveyElementBase = z.object({
|
||||||
id: ZSurveyElementId,
|
id: ZSurveyElementId,
|
||||||
type: z.nativeEnum(TSurveyElementTypeEnum),
|
type: z.nativeEnum(TSurveyElementTypeEnum),
|
||||||
@@ -80,6 +97,7 @@ export const ZSurveyOpenTextElement = ZSurveyElementBase.extend({
|
|||||||
max: z.number().optional(),
|
max: z.number().optional(),
|
||||||
})
|
})
|
||||||
.default({ enabled: false }),
|
.default({ enabled: false }),
|
||||||
|
validationRules: ZValidationRulesForOpenText.optional(),
|
||||||
}).superRefine((data, ctx) => {
|
}).superRefine((data, ctx) => {
|
||||||
if (data.charLimit.enabled && data.charLimit.min === undefined && data.charLimit.max === undefined) {
|
if (data.charLimit.enabled && data.charLimit.min === undefined && data.charLimit.max === undefined) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
@@ -116,6 +134,7 @@ export type TSurveyOpenTextElement = z.infer<typeof ZSurveyOpenTextElement>;
|
|||||||
export const ZSurveyConsentElement = ZSurveyElementBase.extend({
|
export const ZSurveyConsentElement = ZSurveyElementBase.extend({
|
||||||
type: z.literal(TSurveyElementTypeEnum.Consent),
|
type: z.literal(TSurveyElementTypeEnum.Consent),
|
||||||
label: ZI18nString,
|
label: ZI18nString,
|
||||||
|
validationRules: ZValidationRulesForConsent.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSurveyConsentElement = z.infer<typeof ZSurveyConsentElement>;
|
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 const ZShuffleOption = z.enum(["none", "all", "exceptLast"]);
|
||||||
export type TShuffleOption = z.infer<typeof ZShuffleOption>;
|
export type TShuffleOption = z.infer<typeof ZShuffleOption>;
|
||||||
|
|
||||||
export const ZSurveyMultipleChoiceElement = ZSurveyElementBase.extend({
|
// Multiple Choice Single Element
|
||||||
type: z.union([
|
export const ZSurveyMultipleChoiceSingleElement = ZSurveyElementBase.extend({
|
||||||
z.literal(TSurveyElementTypeEnum.MultipleChoiceSingle),
|
type: z.literal(TSurveyElementTypeEnum.MultipleChoiceSingle),
|
||||||
z.literal(TSurveyElementTypeEnum.MultipleChoiceMulti),
|
|
||||||
]),
|
|
||||||
choices: z
|
choices: z
|
||||||
.array(ZSurveyElementChoice)
|
.array(ZSurveyElementChoice)
|
||||||
.min(2, { message: "Multiple Choice Element must have at least two choices" }),
|
.min(2, { message: "Multiple Choice Element must have at least two choices" }),
|
||||||
shuffleOption: ZShuffleOption.optional(),
|
shuffleOption: ZShuffleOption.optional(),
|
||||||
otherOptionPlaceholder: ZI18nString.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>;
|
export type TSurveyMultipleChoiceElement = z.infer<typeof ZSurveyMultipleChoiceElement>;
|
||||||
|
|
||||||
// NPS Element
|
// NPS Element
|
||||||
@@ -151,6 +186,7 @@ export const ZSurveyNPSElement = ZSurveyElementBase.extend({
|
|||||||
lowerLabel: ZI18nString.optional(),
|
lowerLabel: ZI18nString.optional(),
|
||||||
upperLabel: ZI18nString.optional(),
|
upperLabel: ZI18nString.optional(),
|
||||||
isColorCodingEnabled: z.boolean().optional().default(false),
|
isColorCodingEnabled: z.boolean().optional().default(false),
|
||||||
|
validationRules: ZValidationRulesForNPS.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSurveyNPSElement = z.infer<typeof ZSurveyNPSElement>;
|
export type TSurveyNPSElement = z.infer<typeof ZSurveyNPSElement>;
|
||||||
@@ -202,6 +238,7 @@ export const ZSurveyRatingElement = ZSurveyElementBase.extend({
|
|||||||
lowerLabel: ZI18nString.optional(),
|
lowerLabel: ZI18nString.optional(),
|
||||||
upperLabel: ZI18nString.optional(),
|
upperLabel: ZI18nString.optional(),
|
||||||
isColorCodingEnabled: z.boolean().optional().default(false),
|
isColorCodingEnabled: z.boolean().optional().default(false),
|
||||||
|
validationRules: ZValidationRulesForRating.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSurveyRatingElement = z.infer<typeof ZSurveyRatingElement>;
|
export type TSurveyRatingElement = z.infer<typeof ZSurveyRatingElement>;
|
||||||
@@ -220,6 +257,7 @@ export const ZSurveyPictureSelectionElement = ZSurveyElementBase.extend({
|
|||||||
choices: z
|
choices: z
|
||||||
.array(ZSurveyPictureChoice)
|
.array(ZSurveyPictureChoice)
|
||||||
.min(2, { message: "Picture Selection element must have a minimum of 2 choices" }),
|
.min(2, { message: "Picture Selection element must have a minimum of 2 choices" }),
|
||||||
|
validationRules: ZValidationRulesForPictureSelection.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSurveyPictureSelectionElement = z.infer<typeof ZSurveyPictureSelectionElement>;
|
export type TSurveyPictureSelectionElement = z.infer<typeof ZSurveyPictureSelectionElement>;
|
||||||
@@ -229,6 +267,7 @@ export const ZSurveyDateElement = ZSurveyElementBase.extend({
|
|||||||
type: z.literal(TSurveyElementTypeEnum.Date),
|
type: z.literal(TSurveyElementTypeEnum.Date),
|
||||||
html: ZI18nString.optional(),
|
html: ZI18nString.optional(),
|
||||||
format: z.enum(["M-d-y", "d-M-y", "y-M-d"]),
|
format: z.enum(["M-d-y", "d-M-y", "y-M-d"]),
|
||||||
|
validationRules: ZValidationRulesForDate.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSurveyDateElement = z.infer<typeof ZSurveyDateElement>;
|
export type TSurveyDateElement = z.infer<typeof ZSurveyDateElement>;
|
||||||
@@ -239,6 +278,7 @@ export const ZSurveyFileUploadElement = ZSurveyElementBase.extend({
|
|||||||
allowMultipleFiles: z.boolean(),
|
allowMultipleFiles: z.boolean(),
|
||||||
maxSizeInMB: z.number().optional(),
|
maxSizeInMB: z.number().optional(),
|
||||||
allowedFileExtensions: z.array(ZAllowedFileExtension).optional(),
|
allowedFileExtensions: z.array(ZAllowedFileExtension).optional(),
|
||||||
|
validationRules: ZValidationRulesForFileUpload.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSurveyFileUploadElement = z.infer<typeof ZSurveyFileUploadElement>;
|
export type TSurveyFileUploadElement = z.infer<typeof ZSurveyFileUploadElement>;
|
||||||
@@ -248,6 +288,7 @@ export const ZSurveyCalElement = ZSurveyElementBase.extend({
|
|||||||
type: z.literal(TSurveyElementTypeEnum.Cal),
|
type: z.literal(TSurveyElementTypeEnum.Cal),
|
||||||
calUserName: z.string().min(1, { message: "Cal user name is required" }),
|
calUserName: z.string().min(1, { message: "Cal user name is required" }),
|
||||||
calHost: z.string().optional(),
|
calHost: z.string().optional(),
|
||||||
|
validationRules: ZValidationRulesForCal.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSurveyCalElement = z.infer<typeof ZSurveyCalElement>;
|
export type TSurveyCalElement = z.infer<typeof ZSurveyCalElement>;
|
||||||
@@ -265,6 +306,7 @@ export const ZSurveyMatrixElement = ZSurveyElementBase.extend({
|
|||||||
rows: z.array(ZSurveyMatrixElementChoice),
|
rows: z.array(ZSurveyMatrixElementChoice),
|
||||||
columns: z.array(ZSurveyMatrixElementChoice),
|
columns: z.array(ZSurveyMatrixElementChoice),
|
||||||
shuffleOption: ZShuffleOption.optional().default("none"),
|
shuffleOption: ZShuffleOption.optional().default("none"),
|
||||||
|
validationRules: ZValidationRulesForMatrix.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSurveyMatrixElement = z.infer<typeof ZSurveyMatrixElement>;
|
export type TSurveyMatrixElement = z.infer<typeof ZSurveyMatrixElement>;
|
||||||
@@ -286,6 +328,7 @@ export const ZSurveyAddressElement = ZSurveyElementBase.extend({
|
|||||||
state: ZToggleInputConfig,
|
state: ZToggleInputConfig,
|
||||||
zip: ZToggleInputConfig,
|
zip: ZToggleInputConfig,
|
||||||
country: ZToggleInputConfig,
|
country: ZToggleInputConfig,
|
||||||
|
validationRules: ZValidationRulesForAddress.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSurveyAddressElement = z.infer<typeof ZSurveyAddressElement>;
|
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" }),
|
.max(25, { message: "Ranking Element can have at most 25 options" }),
|
||||||
otherOptionPlaceholder: ZI18nString.optional(),
|
otherOptionPlaceholder: ZI18nString.optional(),
|
||||||
shuffleOption: ZShuffleOption.optional(),
|
shuffleOption: ZShuffleOption.optional(),
|
||||||
|
validationRules: ZValidationRulesForRanking.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSurveyRankingElement = z.infer<typeof ZSurveyRankingElement>;
|
export type TSurveyRankingElement = z.infer<typeof ZSurveyRankingElement>;
|
||||||
@@ -311,6 +355,7 @@ export const ZSurveyContactInfoElement = ZSurveyElementBase.extend({
|
|||||||
email: ZToggleInputConfig,
|
email: ZToggleInputConfig,
|
||||||
phone: ZToggleInputConfig,
|
phone: ZToggleInputConfig,
|
||||||
company: ZToggleInputConfig,
|
company: ZToggleInputConfig,
|
||||||
|
validationRules: ZValidationRulesForContactInfo.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSurveyContactInfoElement = z.infer<typeof ZSurveyContactInfoElement>;
|
export type TSurveyContactInfoElement = z.infer<typeof ZSurveyContactInfoElement>;
|
||||||
@@ -319,7 +364,8 @@ export type TSurveyContactInfoElement = z.infer<typeof ZSurveyContactInfoElement
|
|||||||
export const ZSurveyElement = z.union([
|
export const ZSurveyElement = z.union([
|
||||||
ZSurveyOpenTextElement,
|
ZSurveyOpenTextElement,
|
||||||
ZSurveyConsentElement,
|
ZSurveyConsentElement,
|
||||||
ZSurveyMultipleChoiceElement,
|
ZSurveyMultipleChoiceSingleElement,
|
||||||
|
ZSurveyMultipleChoiceMultiElement,
|
||||||
ZSurveyNPSElement,
|
ZSurveyNPSElement,
|
||||||
ZSurveyCTAElement,
|
ZSurveyCTAElement,
|
||||||
ZSurveyRatingElement,
|
ZSurveyRatingElement,
|
||||||
|
|||||||
@@ -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