diff --git a/apps/web/i18n.lock b/apps/web/i18n.lock index 1ff843fbed..8ea152bdc7 100644 --- a/apps/web/i18n.lock +++ b/apps/web/i18n.lock @@ -1127,6 +1127,7 @@ checksums: environments/surveys/edit/add_photo_or_video: 7fd213e807ad060e415d1d4195397473 environments/surveys/edit/add_pin: 1bc282dd7eaea51301655d3e8dd3a9fb environments/surveys/edit/add_question_below: 58e64eb2e013f1175ea0dcf79149109f + environments/surveys/edit/add_question_to_block: 8589b1042aa93531a836549d6036492c environments/surveys/edit/add_row: a613cef4caf1f0e05697c8de5164e2a3 environments/surveys/edit/add_variable: 23f97e23aba763cc58934df4fa13ffc1 environments/surveys/edit/address_fields: 9cabb97c3deaff4f6cb3afc3d5cfaf0a @@ -1158,6 +1159,8 @@ checksums: environments/surveys/edit/bold: 4d7306bc355ed2befd6a9237c5452ee6 environments/surveys/edit/brand_color: 84ddb5736deb9f5c081ffe4962a6c63e environments/surveys/edit/brightness: 45425b6db1872225bfff71cf619d0e64 + environments/surveys/edit/button_external: d2de24e06574622baf1c0cdd1b718b1a + environments/surveys/edit/button_external_description: cbd10d494a70b362bfee811e012c45b1 environments/surveys/edit/button_label: db3cd7c74f393187bd780c5c3d8b9b4f environments/surveys/edit/button_url: 6f39f649a165a11873c11ea6403dba90 environments/surveys/edit/cal_username: a4a9c739af909d975beb1bc4998feae9 @@ -1221,6 +1224,7 @@ checksums: environments/surveys/edit/create_group: 4566e056e5217dc02a383105892fe18c environments/surveys/edit/create_your_own_survey: e3ddd53e0cfa409ca8dccfb3d77933e7 environments/surveys/edit/css_selector: 615e9f1b74622df29de28a5b5614c6fe + environments/surveys/edit/cta_button_label: ec070ffba38eae24751bb3a4c1e14c81 environments/surveys/edit/custom_hostname: bc2b1c8de3f9b8ef145b45aeba6ab429 environments/surveys/edit/darken_or_lighten_background_of_your_choice: 304a64a8050ebf501d195e948cd25b6f environments/surveys/edit/date_format: e95dfc41ac944874868487457ddc057a @@ -1239,9 +1243,11 @@ checksums: environments/surveys/edit/does_not_include_one_of: 91090d2e0667faf654f6a81d9857440f environments/surveys/edit/does_not_start_with: 9395869b54cdfb353a51a7e0864f4fd7 environments/surveys/edit/duplicate_block: d4ea4afb5fc5b18a81cbe0302fa05997 + environments/surveys/edit/duplicate_question: 910751de01fdd327165968214717711b environments/surveys/edit/edit_link: 40ba9e15beac77a46c5baf30be84ac54 environments/surveys/edit/edit_recall: 38a4a7378d02453e35d06f2532eef318 environments/surveys/edit/edit_translations: 2b21bea4b53e88342559272701e9fbf3 + environments/surveys/edit/element_not_found: 196777ff6811dd177971ffc8e27a72c1 environments/surveys/edit/enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey: 71977f91ec151b61ee3528ac2618afed environments/surveys/edit/enable_recaptcha_to_protect_your_survey_from_spam: 4483a5763718d201ac97caa1e1216e13 environments/surveys/edit/enable_spam_protection: e1fb0dd0723044bf040b92d8fc58015d @@ -1317,11 +1323,12 @@ checksums: environments/surveys/edit/hidden_field_used_in_recall: 70dee46bae18209e8861b654ff9a04ae environments/surveys/edit/hidden_field_used_in_recall_ending_card: a985d03d18e33d83521961c9c981d0ee environments/surveys/edit/hidden_field_used_in_recall_welcome: 22fef7001d5e60edbf877e7b435c1991 - environments/surveys/edit/hide_advanced_settings: ffa251d7762030b72c12e92f3c69a9b4 environments/surveys/edit/hide_back_button: 9f355fb4a8e80485b9de521a952ffeb9 environments/surveys/edit/hide_back_button_description: caaa30cf43c5611577933a1c9f44b9ee + environments/surveys/edit/hide_block_settings: c24c3d3892c251792e297cdc036d2fde environments/surveys/edit/hide_logo: eef4de2e3fffe8cbe32bff4f6f7250d8 environments/surveys/edit/hide_progress_bar: 7eefe7db6a051105bded521d94204933 + environments/surveys/edit/hide_question_settings: 99127cd016db2f7fc80333b36473c0ef environments/surveys/edit/hide_the_logo_in_this_specific_survey: 29d4c6c714886e57bc29ad292d0f5a00 environments/surveys/edit/hostname: 9bdaa7692869999df51bb60d58d9ef62 environments/surveys/edit/how_funky_do_you_want_your_cards_in_survey_type_derived_surveys: 3cb16b37510c01af20a80f51b598346e @@ -1354,7 +1361,7 @@ checksums: environments/surveys/edit/is_skipped: 9fb90b6578f603cca37d4e6c912bb401 environments/surveys/edit/is_submitted: 13e774a97ad5f5609555e6f99514e70f environments/surveys/edit/italic: 555c60fb1d12ae305136202afa6deb3d - environments/surveys/edit/jump_to_question: 742aabed8845190825418aa429f01b2d + environments/surveys/edit/jump_to_block: 2fc00bd725c44f98861051c57bb2c392 environments/surveys/edit/keep_current_order: a7c944ad6b3515f2c4f83a2c81f8fc26 environments/surveys/edit/keep_showing_while_conditions_match: 2574802d87bd6da151c9145aacce7281 environments/surveys/edit/key: 3d1065ab98a1c2f1210507fd5c7bf515 @@ -1368,16 +1375,18 @@ checksums: environments/surveys/edit/logic_error_warning: 542fbb918ffdb29e6f9a4a6196ffb558 environments/surveys/edit/logic_error_warning_text: f2afad8852a95ed169a39959efbf592c environments/surveys/edit/long_answer: 3a97f8d2e90aba6e679917a0c5670c53 + environments/surveys/edit/long_answer_toggle_description: 86bcdfeb74d9825c2f2d5a215e92d111 environments/surveys/edit/lower_label: 45985bca022d4370bd6e013af75d5160 environments/surveys/edit/manage_languages: 9c56d5afee8a73dfc283a452470f3a10 environments/surveys/edit/matrix_all_fields: 187240509163b2f52a400a565e57c67f environments/surveys/edit/matrix_rows: 8f41f34e6ca28221cf1ebd948af4c151 environments/surveys/edit/max_file_size: 3d35a22048f4d22e24da698fb5fb77d7 environments/surveys/edit/max_file_size_limit_is: 78998639cde3587cecb272ba47e05f9e + environments/surveys/edit/move_question_to_block: e8d7ef1e2f727921cb7f5788849492ad environments/surveys/edit/multiply: 89a0bb629167f97750ae1645a46ced0d environments/surveys/edit/needed_for_self_hosted_cal_com_instance: d241e72f0332177d32ce6c35070757dc + environments/surveys/edit/next_block: 53eaa5b1c9333455ab1e99bedd222ba2 environments/surveys/edit/next_button_label: e23522dd38f3eabeeccd3f48f32b73a8 - environments/surveys/edit/next_question: 2e0f1ea264fb4bfcb8378b2b0cf7c18f environments/surveys/edit/no_hidden_fields_yet_add_first_one_below: 9cc6cab3a6a42dbf835215897b5b8516 environments/surveys/edit/no_images_found_for: 90f10f4611ed7b115a49595409b66ebe environments/surveys/edit/no_languages_found_add_first_one_to_get_started: 22d7782c8504daf693cab3cf7135d6e3 @@ -1483,11 +1492,12 @@ checksums: environments/surveys/edit/set_the_global_placement_in_the_look_feel_settings: e34e579e778a918733702edb041ac929 environments/surveys/edit/settings_saved_successfully: eb109269bc59dd67ae09fd9eb53652d2 environments/surveys/edit/seven_points: 4ead50fdfda45e8710767e1b1a84bf42 - environments/surveys/edit/show_advanced_settings: b6f5bbbb84f34e51cd72ccd332e9613e + environments/surveys/edit/show_block_settings: bad99d99c9908874e45f5c350a88cc79 environments/surveys/edit/show_button: 6b364aac9d7ac71f34a438607c9693bc environments/surveys/edit/show_language_switch: b6915a7f26d7079f2d4d844d74440413 environments/surveys/edit/show_multiple_times: 5e6e0244c20feca78723c79aa1ddcf62 environments/surveys/edit/show_only_once: 31858baf60ebcf193c7e35d9084af0af + environments/surveys/edit/show_question_settings: a84698a95df0833a35d653edcdbbe501 environments/surveys/edit/show_survey_maximum_of: 721ed61b01a9fc8ce4becb72823bb72e environments/surveys/edit/show_survey_to_users: d5e90fd17babfea978fce826e9df89b0 environments/surveys/edit/show_to_x_percentage_of_targeted_users: b745169011fa7e8ca475baa5500c5197 @@ -1513,6 +1523,7 @@ checksums: environments/surveys/edit/survey_placement: 083c10f257337f9648bf9d435b18ec2c environments/surveys/edit/survey_trigger: f0c7014a684ca566698b87074fad5579 environments/surveys/edit/switch_multi_lanugage_on_to_get_started: d2ca06684af26bd6b5121a4656bb6458 + environments/surveys/edit/target_block_not_found: 0a0c401017ab32364fec2fcbf815d832 environments/surveys/edit/targeted: ca615f1fc3b490d5a2187b27fb4a2073 environments/surveys/edit/ten_points: a1317b82003859f77fb3138c55450d63 environments/surveys/edit/the_survey_will_be_shown_multiple_times_until_they_respond: 219b15081cbafaa391e266bd2cc4c9d4 diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json index 6fb95c3cb8..0d4a8c8604 100644 --- a/apps/web/locales/de-DE.json +++ b/apps/web/locales/de-DE.json @@ -1212,6 +1212,7 @@ "add_photo_or_video": "Foto oder Video hinzufügen", "add_pin": "PIN hinzufügen", "add_question_below": "Frage unten hinzufügen", + "add_question_to_block": "Frage zum Block hinzufügen", "add_row": "Zeile hinzufügen", "add_variable": "Variable hinzufügen", "address_fields": "Adressfelder", @@ -1243,6 +1244,8 @@ "bold": "Fett", "brand_color": "Markenfarbe", "brightness": "Helligkeit", + "button_external": "Externen Link aktivieren", + "button_external_description": "Fügen Sie eine Schaltfläche hinzu, die eine externe URL in einem neuen Tab öffnet", "button_label": "Beschriftung", "button_url": "URL", "cal_username": "Cal.com Benutzername oder Benutzername/Ereignis", @@ -1306,6 +1309,7 @@ "create_group": "Gruppe erstellen", "create_your_own_survey": "Erstelle deine eigene Umfrage", "css_selector": "CSS-Selektor", + "cta_button_label": "\"CTA\"-Schaltflächen-Beschriftung", "custom_hostname": "Benutzerdefinierter Hostname", "darken_or_lighten_background_of_your_choice": "Hintergrund deiner Wahl abdunkeln oder aufhellen.", "date_format": "Datumsformat", @@ -1324,9 +1328,11 @@ "does_not_include_one_of": "Enthält nicht eines von", "does_not_start_with": "Fängt nicht an mit", "duplicate_block": "Block duplizieren", + "duplicate_question": "Frage duplizieren", "edit_link": "Bearbeitungslink", "edit_recall": "Erinnerung bearbeiten", "edit_translations": "{lang} -Übersetzungen bearbeiten", + "element_not_found": "Frage nicht gefunden", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Teilnehmer können die Umfragesprache jederzeit während der Umfrage ändern.", "enable_recaptcha_to_protect_your_survey_from_spam": "Spamschutz verwendet reCAPTCHA v3, um Spam-Antworten herauszufiltern.", "enable_spam_protection": "Spamschutz", @@ -1402,11 +1408,12 @@ "hidden_field_used_in_recall": "Verstecktes Feld \"{hiddenField}\" wird in Frage {questionIndex} abgerufen.", "hidden_field_used_in_recall_ending_card": "Verstecktes Feld \"{hiddenField}\" wird in der Abschlusskarte abgerufen.", "hidden_field_used_in_recall_welcome": "Verstecktes Feld \"{hiddenField}\" wird in der Willkommenskarte abgerufen.", - "hide_advanced_settings": "Erweiterte Einstellungen ausblenden", "hide_back_button": "'Zurück'-Button ausblenden", "hide_back_button_description": "Den Zurück-Button in der Umfrage nicht anzeigen", + "hide_block_settings": "Block-Einstellungen ausblenden", "hide_logo": "Logo verstecken", "hide_progress_bar": "Fortschrittsbalken ausblenden", + "hide_question_settings": "Frageeinstellungen ausblenden", "hide_the_logo_in_this_specific_survey": "Logo in dieser speziellen Umfrage verstecken", "hostname": "Hostname", "how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Wie funky sollen deine Karten in {surveyTypeDerived} Umfragen sein", @@ -1439,7 +1446,7 @@ "is_skipped": "Wird übersprungen", "is_submitted": "Wird eingereicht", "italic": "Kursiv", - "jump_to_question": "Zur Frage springen", + "jump_to_block": "Zum Block springen", "keep_current_order": "Bestehende Anordnung beibehalten", "keep_showing_while_conditions_match": "Zeige weiter, solange die Bedingungen übereinstimmen", "key": "Schlüssel", @@ -1453,16 +1460,18 @@ "logic_error_warning": "Änderungen werden zu Logikfehlern führen", "logic_error_warning_text": "Das Ändern des Fragetypen entfernt die Logikbedingungen von dieser Frage", "long_answer": "Lange Antwort", + "long_answer_toggle_description": "Ermöglichen Sie den Befragten, längere Antworten über mehrere Zeilen zu schreiben.", "lower_label": "Unteres Label", "manage_languages": "Sprachen verwalten", "matrix_all_fields": "Alle Felder", "matrix_rows": "Zeilen", "max_file_size": "Max. Dateigröße", "max_file_size_limit_is": "Max. Dateigröße ist", + "move_question_to_block": "Frage in Block verschieben", "multiply": "Multiplizieren *", "needed_for_self_hosted_cal_com_instance": "Benötigt für eine selbstgehostete Cal.com-Instanz", - "next_button_label": "Weiter", - "next_question": "Nächste Frage", + "next_block": "Nächster Block", + "next_button_label": "Beschriftung der Schaltfläche \"Weiter\"", "no_hidden_fields_yet_add_first_one_below": "Noch keine versteckten Felder. Füge das erste unten hinzu.", "no_images_found_for": "Keine Bilder gefunden für ''{query}\"", "no_languages_found_add_first_one_to_get_started": "Keine Sprachen gefunden. Füge die erste hinzu, um loszulegen.", @@ -1570,11 +1579,12 @@ "set_the_global_placement_in_the_look_feel_settings": "Stelle die globale Platzierung in den Look & Feel-Einstellungen ein.", "settings_saved_successfully": "Einstellungen erfolgreich gespeichert", "seven_points": "7 Punkte", - "show_advanced_settings": "Erweiterte Einstellungen anzeigen", + "show_block_settings": "Block-Einstellungen anzeigen", "show_button": "Button anzeigen", "show_language_switch": "Sprachwechsel anzeigen", "show_multiple_times": "Mehrfach anzeigen", "show_only_once": "Nur einmal anzeigen", + "show_question_settings": "Frageeinstellungen anzeigen", "show_survey_maximum_of": "Umfrage maximal anzeigen von", "show_survey_to_users": "Umfrage % der Nutzer anzeigen", "show_to_x_percentage_of_targeted_users": "Zeige {percentage}% der Zielbenutzer", @@ -1600,6 +1610,7 @@ "survey_placement": "Platzierung der Umfrage", "survey_trigger": "Auslöser der Umfrage", "switch_multi_lanugage_on_to_get_started": "Schalte Mehrsprachigkeit ein, um loszulegen 👉", + "target_block_not_found": "Zielblock nicht gefunden", "targeted": "Gezielt", "ten_points": "10 Punkte", "the_survey_will_be_shown_multiple_times_until_they_respond": "Die Umfrage wird mehrmals angezeigt, bis Du antwortest", diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json index a30a356664..1174aedd9e 100644 --- a/apps/web/locales/en-US.json +++ b/apps/web/locales/en-US.json @@ -1212,6 +1212,7 @@ "add_photo_or_video": "Add photo or video", "add_pin": "Add PIN", "add_question_below": "Add question below", + "add_question_to_block": "Add question to block", "add_row": "Add row", "add_variable": "Add variable", "address_fields": "Address Fields", @@ -1243,6 +1244,8 @@ "bold": "Bold", "brand_color": "Brand color", "brightness": "Brightness", + "button_external": "Enable External Link", + "button_external_description": "Add a button that opens an external URL in a new tab", "button_label": "Button Label", "button_url": "Button URL", "cal_username": "Cal.com username or username/event", @@ -1306,6 +1309,7 @@ "create_group": "Create group", "create_your_own_survey": "Create your own survey", "css_selector": "CSS Selector", + "cta_button_label": "\"CTA\" button label", "custom_hostname": "Custom hostname", "darken_or_lighten_background_of_your_choice": "Darken or lighten background of your choice.", "date_format": "Date format", @@ -1324,9 +1328,11 @@ "does_not_include_one_of": "Does not include one of", "does_not_start_with": "Does not start with", "duplicate_block": "Duplicate block", + "duplicate_question": "Duplicate question", "edit_link": "Edit link", "edit_recall": "Edit Recall", "edit_translations": "Edit {lang} translations", + "element_not_found": "Question not found", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Enable participants to switch the survey language at any point during the survey.", "enable_recaptcha_to_protect_your_survey_from_spam": "Spam protection uses reCAPTCHA v3 to filter out the spam responses.", "enable_spam_protection": "Spam protection", @@ -1402,11 +1408,12 @@ "hidden_field_used_in_recall": "Hidden field \"{hiddenField}\" is being recalled in question {questionIndex}.", "hidden_field_used_in_recall_ending_card": "Hidden field \"{hiddenField}\" is being recalled in Ending Card", "hidden_field_used_in_recall_welcome": "Hidden field \"{hiddenField}\" is being recalled in Welcome card.", - "hide_advanced_settings": "Hide advanced settings", "hide_back_button": "Hide 'Back' button", "hide_back_button_description": "Do not display the back button in the survey", + "hide_block_settings": "Hide Block settings", "hide_logo": "Hide logo", "hide_progress_bar": "Hide progress bar", + "hide_question_settings": "Hide Question settings", "hide_the_logo_in_this_specific_survey": "Hide the logo in this specific survey", "hostname": "Hostname", "how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "How funky do you want your cards in {surveyTypeDerived} Surveys", @@ -1439,7 +1446,7 @@ "is_skipped": "Is skipped", "is_submitted": "Is submitted", "italic": "Italic", - "jump_to_question": "Jump to question", + "jump_to_block": "Jump to block", "keep_current_order": "Keep current order", "keep_showing_while_conditions_match": "Keep showing while conditions match", "key": "Key", @@ -1453,16 +1460,18 @@ "logic_error_warning": "Changing will cause logic errors", "logic_error_warning_text": "Changing the question type will remove the logic conditions from this question", "long_answer": "Long answer", + "long_answer_toggle_description": "Allow respondents to write longer, multi-line answers.", "lower_label": "Lower Label", "manage_languages": "Manage Languages", "matrix_all_fields": "All fields", "matrix_rows": "Rows", "max_file_size": "Max file size", "max_file_size_limit_is": "Max file size limit is", + "move_question_to_block": "Move question to block", "multiply": "Multiply *", "needed_for_self_hosted_cal_com_instance": "Needed for a self-hosted Cal.com instance", + "next_block": "Next block", "next_button_label": "\"Next\" button label", - "next_question": "Next question", "no_hidden_fields_yet_add_first_one_below": "No hidden fields yet. Add the first one below.", "no_images_found_for": "No images found for ''{query}\"", "no_languages_found_add_first_one_to_get_started": "No languages found. Add the first one to get started.", @@ -1570,11 +1579,12 @@ "set_the_global_placement_in_the_look_feel_settings": "Set the global placement in the Look & Feel settings.", "settings_saved_successfully": "Settings saved successfully.", "seven_points": "7 points", - "show_advanced_settings": "Show Advanced settings", + "show_block_settings": "Show Block settings", "show_button": "Show Button", "show_language_switch": "Show language switch", "show_multiple_times": "Show multiple times", "show_only_once": "Show only once", + "show_question_settings": "Show Question settings", "show_survey_maximum_of": "Show survey maximum of", "show_survey_to_users": "Show survey to % of users", "show_to_x_percentage_of_targeted_users": "Show to {percentage}% of targeted users", @@ -1600,6 +1610,7 @@ "survey_placement": "Survey Placement", "survey_trigger": "Survey Trigger", "switch_multi_lanugage_on_to_get_started": "Switch multi-lanugage on to get started \uD83D\uDC49", + "target_block_not_found": "Target block not found", "targeted": "Targeted", "ten_points": "10 points", "the_survey_will_be_shown_multiple_times_until_they_respond": "The survey will be shown multiple times until they respond", diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json index a5290646db..ca1c7f9eb1 100644 --- a/apps/web/locales/fr-FR.json +++ b/apps/web/locales/fr-FR.json @@ -1212,6 +1212,7 @@ "add_photo_or_video": "Ajouter une photo ou une vidéo", "add_pin": "Ajouter un code PIN", "add_question_below": "Ajouter une question ci-dessous", + "add_question_to_block": "Ajouter une question au bloc", "add_row": "Ajouter une ligne", "add_variable": "Ajouter une variable", "address_fields": "Champs d'adresse", @@ -1243,6 +1244,8 @@ "bold": "Gras", "brand_color": "Couleur de marque", "brightness": "Luminosité", + "button_external": "Activer le lien externe", + "button_external_description": "Ajouter un bouton qui ouvre une URL externe dans un nouvel onglet", "button_label": "Label du bouton", "button_url": "URL du bouton", "cal_username": "Nom d'utilisateur Cal.com ou nom d'utilisateur/événement", @@ -1306,6 +1309,7 @@ "create_group": "Créer un groupe", "create_your_own_survey": "Créez votre propre enquête", "css_selector": "Sélecteur CSS", + "cta_button_label": "Libellé du bouton « CTA »", "custom_hostname": "Nom d'hôte personnalisé", "darken_or_lighten_background_of_your_choice": "Assombrir ou éclaircir l'arrière-plan de votre choix.", "date_format": "Format de date", @@ -1324,9 +1328,11 @@ "does_not_include_one_of": "n'inclut pas un de", "does_not_start_with": "Ne commence pas par", "duplicate_block": "Dupliquer le bloc", + "duplicate_question": "Dupliquer la question", "edit_link": "Modifier le lien", "edit_recall": "Modifier le rappel", "edit_translations": "Modifier les traductions {lang}", + "element_not_found": "Question non trouvée", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permettre aux participants de changer la langue de l'enquête à tout moment pendant celle-ci.", "enable_recaptcha_to_protect_your_survey_from_spam": "La protection contre le spam utilise reCAPTCHA v3 pour filtrer les réponses indésirables.", "enable_spam_protection": "Protection contre le spam", @@ -1402,11 +1408,12 @@ "hidden_field_used_in_recall": "Le champ caché \"{hiddenField}\" est rappelé dans la question {questionIndex}.", "hidden_field_used_in_recall_ending_card": "Le champ caché \"{hiddenField}\" est rappelé dans la carte de fin.", "hidden_field_used_in_recall_welcome": "Le champ caché \"{hiddenField}\" est rappelé dans la carte de bienvenue.", - "hide_advanced_settings": "Cacher les paramètres avancés", "hide_back_button": "Masquer le bouton 'Retour'", "hide_back_button_description": "Ne pas afficher le bouton retour dans l'enquête", + "hide_block_settings": "Masquer les paramètres du bloc", "hide_logo": "Cacher le logo", "hide_progress_bar": "Cacher la barre de progression", + "hide_question_settings": "Masquer les paramètres de la question", "hide_the_logo_in_this_specific_survey": "Cacher le logo dans cette enquête spécifique", "hostname": "Nom d'hôte", "how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "À quel point voulez-vous que vos cartes soient funky dans les enquêtes {surveyTypeDerived}", @@ -1439,7 +1446,7 @@ "is_skipped": "Est ignoré", "is_submitted": "Est soumis", "italic": "Italique", - "jump_to_question": "Passer à la question", + "jump_to_block": "Aller au bloc", "keep_current_order": "Conserver la commande actuelle", "keep_showing_while_conditions_match": "Continuer à afficher tant que les conditions correspondent", "key": "Clé", @@ -1453,16 +1460,18 @@ "logic_error_warning": "Changer causera des erreurs logiques", "logic_error_warning_text": "Changer le type de question supprimera les conditions logiques de cette question.", "long_answer": "Longue réponse", + "long_answer_toggle_description": "Permettre aux répondants d'écrire des réponses plus longues et sur plusieurs lignes.", "lower_label": "Étiquette inférieure", "manage_languages": "Gérer les langues", "matrix_all_fields": "Tous les champs", "matrix_rows": "Lignes", "max_file_size": "Taille maximale du fichier", "max_file_size_limit_is": "La taille maximale du fichier est", + "move_question_to_block": "Déplacer la question vers le bloc", "multiply": "Multiplier *", "needed_for_self_hosted_cal_com_instance": "Nécessaire pour une instance Cal.com auto-hébergée", - "next_button_label": "Label du bouton \"Suivant\"", - "next_question": "Question suivante", + "next_block": "Bloc suivant", + "next_button_label": "Libellé du bouton « Suivant »", "no_hidden_fields_yet_add_first_one_below": "Aucun champ caché pour le moment. Ajoutez le premier ci-dessous.", "no_images_found_for": "Aucune image trouvée pour ''{query}\"", "no_languages_found_add_first_one_to_get_started": "Aucune langue trouvée. Ajoutez la première pour commencer.", @@ -1570,11 +1579,12 @@ "set_the_global_placement_in_the_look_feel_settings": "Définissez le placement global dans les paramètres d'apparence.", "settings_saved_successfully": "Paramètres enregistrés avec succès", "seven_points": "7 points", - "show_advanced_settings": "Afficher les paramètres avancés", + "show_block_settings": "Afficher les paramètres du bloc", "show_button": "Afficher le bouton", "show_language_switch": "Afficher le changement de langue", "show_multiple_times": "Afficher plusieurs fois", "show_only_once": "Afficher une seule fois", + "show_question_settings": "Afficher les paramètres de la question", "show_survey_maximum_of": "Afficher le maximum du sondage de", "show_survey_to_users": "Afficher l'enquête à % des utilisateurs", "show_to_x_percentage_of_targeted_users": "Afficher à {percentage}% des utilisateurs ciblés", @@ -1600,6 +1610,7 @@ "survey_placement": "Placement de l'enquête", "survey_trigger": "Déclencheur d'enquête", "switch_multi_lanugage_on_to_get_started": "Activez le multilingue pour commencer 👉", + "target_block_not_found": "Bloc cible non trouvé", "targeted": "Ciblé", "ten_points": "10 points", "the_survey_will_be_shown_multiple_times_until_they_respond": "L'enquête sera affichée plusieurs fois jusqu'à ce qu'ils répondent.", diff --git a/apps/web/locales/ja-JP.json b/apps/web/locales/ja-JP.json index 4b54788aea..2f343a1b23 100644 --- a/apps/web/locales/ja-JP.json +++ b/apps/web/locales/ja-JP.json @@ -1212,6 +1212,7 @@ "add_photo_or_video": "写真または動画を追加", "add_pin": "PINを追加", "add_question_below": "以下に質問を追加", + "add_question_to_block": "ブロックに質問を追加", "add_row": "行を追加", "add_variable": "変数を追加", "address_fields": "住所フィールド", @@ -1243,6 +1244,8 @@ "bold": "太字", "brand_color": "ブランドカラー", "brightness": "明るさ", + "button_external": "外部リンクを有効にする", + "button_external_description": "新しいタブで外部URLを開くボタンを追加する", "button_label": "ボタンのラベル", "button_url": "ボタンURL", "cal_username": "Cal.comのユーザー名またはユーザー名/イベント", @@ -1306,6 +1309,7 @@ "create_group": "グループを作成", "create_your_own_survey": "独自のフォームを作成", "css_selector": "CSSセレクター", + "cta_button_label": "\"CTA\"ボタンのラベル", "custom_hostname": "カスタムホスト名", "darken_or_lighten_background_of_your_choice": "お好みの背景を暗くしたり明るくしたりします。", "date_format": "日付形式", @@ -1324,9 +1328,11 @@ "does_not_include_one_of": "のいずれも含まない", "does_not_start_with": "で始まらない", "duplicate_block": "ブロックを複製", + "duplicate_question": "質問を複製", "edit_link": "編集 リンク", "edit_recall": "リコールを編集", "edit_translations": "{lang} 翻訳を編集", + "element_not_found": "質問が見つかりません", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "回答者がフォームの途中でいつでも言語を切り替えられるようにします。", "enable_recaptcha_to_protect_your_survey_from_spam": "スパム対策はreCAPTCHA v3を使用してスパム回答をフィルタリングします。", "enable_spam_protection": "スパム対策", @@ -1402,11 +1408,12 @@ "hidden_field_used_in_recall": "隠し フィールド \"{hiddenField}\" が 質問 {questionIndex} で 呼び出され て います 。", "hidden_field_used_in_recall_ending_card": "隠し フィールド \"{hiddenField}\" が エンディング カード で 呼び出され て います。", "hidden_field_used_in_recall_welcome": "隠し フィールド \"{hiddenField}\" が ウェルカム カード で 呼び出され て います。", - "hide_advanced_settings": "詳細設定を非表示", "hide_back_button": "「戻る」ボタンを非表示", "hide_back_button_description": "フォームに「戻る」ボタンを表示しない", + "hide_block_settings": "ブロック設定を非表示", "hide_logo": "ロゴを非表示", "hide_progress_bar": "プログレスバーを非表示", + "hide_question_settings": "質問設定を非表示", "hide_the_logo_in_this_specific_survey": "この特定のフォームでロゴを非表示にする", "hostname": "ホスト名", "how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "{surveyTypeDerived} フォームのカードをどれくらいユニークにしますか", @@ -1439,7 +1446,7 @@ "is_skipped": "スキップ済み", "is_submitted": "送信済み", "italic": "イタリック", - "jump_to_question": "質問にジャンプ", + "jump_to_block": "ブロックへジャンプ", "keep_current_order": "現在の順序を維持", "keep_showing_while_conditions_match": "条件が一致する間、表示し続ける", "key": "キー", @@ -1453,16 +1460,18 @@ "logic_error_warning": "変更するとロジックエラーが発生します", "logic_error_warning_text": "質問の種類を変更すると、この質問のロジック条件が削除されます", "long_answer": "長文回答", + "long_answer_toggle_description": "回答者が長文の複数行の回答を書けるようにします。", "lower_label": "下限ラベル", "manage_languages": "言語を管理", "matrix_all_fields": "すべてのフィールド", "matrix_rows": "行", "max_file_size": "最大ファイルサイズ", "max_file_size_limit_is": "最大ファイルサイズの上限は", + "move_question_to_block": "質問をブロックに移動", "multiply": "乗算 *", "needed_for_self_hosted_cal_com_instance": "セルフホストのCal.comインスタンスに必要", + "next_block": "次のブロック", "next_button_label": "「次へ」ボタンのラベル", - "next_question": "次の質問", "no_hidden_fields_yet_add_first_one_below": "まだ非表示フィールドがありません。以下で最初のものを追加してください。", "no_images_found_for": "''{query}'' の画像が見つかりません", "no_languages_found_add_first_one_to_get_started": "言語が見つかりません。始めるには、最初のものを追加してください。", @@ -1570,11 +1579,12 @@ "set_the_global_placement_in_the_look_feel_settings": "「デザイン」設定でグローバルな配置を設定します。", "settings_saved_successfully": "設定を正常に保存しました。", "seven_points": "7点", - "show_advanced_settings": "詳細設定を表示", + "show_block_settings": "ブロック設定を表示", "show_button": "ボタンを表示", "show_language_switch": "言語切り替えを表示", "show_multiple_times": "複数回表示", "show_only_once": "一度だけ表示", + "show_question_settings": "質問設定を表示", "show_survey_maximum_of": "フォームの最大表示回数", "show_survey_to_users": "ユーザーの {percentage}% にフォームを表示", "show_to_x_percentage_of_targeted_users": "ターゲットユーザーの {percentage}% に表示", @@ -1600,6 +1610,7 @@ "survey_placement": "フォームの配置", "survey_trigger": "フォームのトリガー", "switch_multi_lanugage_on_to_get_started": "始めるには多言語をオンにしてください 👉", + "target_block_not_found": "対象ブロックが見つかりません", "targeted": "ターゲット", "ten_points": "10点", "the_survey_will_be_shown_multiple_times_until_they_respond": "回答するまで複数回フォームが表示されます", diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json index 97ae84bce9..53820e414c 100644 --- a/apps/web/locales/pt-BR.json +++ b/apps/web/locales/pt-BR.json @@ -1212,6 +1212,7 @@ "add_photo_or_video": "Adicionar foto ou video", "add_pin": "Adicionar PIN", "add_question_below": "Adicione a pergunta abaixo", + "add_question_to_block": "Adicionar pergunta ao bloco", "add_row": "Adicionar linha", "add_variable": "Adicionar variável", "address_fields": "Campos de Endereço", @@ -1243,6 +1244,8 @@ "bold": "Negrito", "brand_color": "Cor da marca", "brightness": "brilho", + "button_external": "Habilitar link externo", + "button_external_description": "Adicionar um botão que abre uma URL externa em uma nova aba", "button_label": "Rótulo do Botão", "button_url": "URL do Botão", "cal_username": "Nome de usuário do Cal.com ou nome de usuário/evento", @@ -1306,6 +1309,7 @@ "create_group": "Criar grupo", "create_your_own_survey": "Crie sua própria pesquisa", "css_selector": "Seletor CSS", + "cta_button_label": "Rótulo do botão \"CTA\"", "custom_hostname": "Hostname personalizado", "darken_or_lighten_background_of_your_choice": "Escureça ou clareie o fundo da sua escolha.", "date_format": "Formato de data", @@ -1324,9 +1328,11 @@ "does_not_include_one_of": "Não inclui um de", "does_not_start_with": "Não começa com", "duplicate_block": "Duplicar bloco", + "duplicate_question": "Duplicar pergunta", "edit_link": "Editar link", "edit_recall": "Editar Lembrete", "edit_translations": "Editar traduções de {lang}", + "element_not_found": "Pergunta não encontrada", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permitir que os participantes mudem o idioma da pesquisa a qualquer momento durante a pesquisa.", "enable_recaptcha_to_protect_your_survey_from_spam": "A proteção contra spam usa o reCAPTCHA v3 para filtrar as respostas de spam.", "enable_spam_protection": "Proteção contra spam", @@ -1402,11 +1408,12 @@ "hidden_field_used_in_recall": "Campo oculto \"{hiddenField}\" está sendo recordado na pergunta {questionIndex}.", "hidden_field_used_in_recall_ending_card": "Campo oculto \"{hiddenField}\" está sendo recordado no card de Encerramento.", "hidden_field_used_in_recall_welcome": "Campo oculto \"{hiddenField}\" está sendo recordado no card de Boas-Vindas.", - "hide_advanced_settings": "Ocultar configurações avançadas", "hide_back_button": "Ocultar botão 'Voltar'", "hide_back_button_description": "Não exibir o botão de voltar na pesquisa", + "hide_block_settings": "Ocultar configurações do bloco", "hide_logo": "Esconder logo", "hide_progress_bar": "Esconder barra de progresso", + "hide_question_settings": "Ocultar configurações da pergunta", "hide_the_logo_in_this_specific_survey": "Esconder o logo nessa pesquisa específica", "hostname": "nome do host", "how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Quão descoladas você quer suas cartas em Pesquisas {surveyTypeDerived}", @@ -1439,7 +1446,7 @@ "is_skipped": "é pulado", "is_submitted": "é submetido", "italic": "Itálico", - "jump_to_question": "Pular para a pergunta", + "jump_to_block": "Pular para o bloco", "keep_current_order": "Manter pedido atual", "keep_showing_while_conditions_match": "Continue mostrando enquanto as condições corresponderem", "key": "chave", @@ -1453,16 +1460,18 @@ "logic_error_warning": "Mudar vai causar erros de lógica", "logic_error_warning_text": "Mudar o tipo de pergunta vai remover as condições lógicas dessa pergunta", "long_answer": "resposta longa", + "long_answer_toggle_description": "Permitir que os respondentes escrevam respostas mais longas e com várias linhas.", "lower_label": "Etiqueta Inferior", "manage_languages": "Gerenciar Idiomas", "matrix_all_fields": "Todos os campos", "matrix_rows": "Linhas", "max_file_size": "Tamanho máximo do arquivo", "max_file_size_limit_is": "Tamanho máximo do arquivo é", + "move_question_to_block": "Mover pergunta para o bloco", "multiply": "Multiplicar *", "needed_for_self_hosted_cal_com_instance": "Necessário para uma instância auto-hospedada do Cal.com", + "next_block": "Próximo bloco", "next_button_label": "Próximo", - "next_question": "próxima pergunta", "no_hidden_fields_yet_add_first_one_below": "Ainda não há campos ocultos. Adicione o primeiro abaixo.", "no_images_found_for": "Nenhuma imagem encontrada para ''{query}\"", "no_languages_found_add_first_one_to_get_started": "Nenhum idioma encontrado. Adicione o primeiro para começar.", @@ -1570,11 +1579,12 @@ "set_the_global_placement_in_the_look_feel_settings": "Defina o posicionamento global nas configurações de Aparência.", "settings_saved_successfully": "Configurações salvas com sucesso", "seven_points": "7 pontos", - "show_advanced_settings": "Mostrar configurações avançadas", + "show_block_settings": "Mostrar configurações do bloco", "show_button": "Mostrar Botão", "show_language_switch": "Mostrar troca de idioma", "show_multiple_times": "Mostrar várias vezes", "show_only_once": "Mostrar só uma vez", + "show_question_settings": "Mostrar configurações da pergunta", "show_survey_maximum_of": "Mostrar no máximo", "show_survey_to_users": "Mostrar pesquisa para % dos usuários", "show_to_x_percentage_of_targeted_users": "Mostrar para {percentage}% dos usuários segmentados", @@ -1600,6 +1610,7 @@ "survey_placement": "Posicionamento da Pesquisa", "survey_trigger": "Gatilho de Pesquisa", "switch_multi_lanugage_on_to_get_started": "Ative o modo multilíngue para começar 👉", + "target_block_not_found": "Bloco de destino não encontrado", "targeted": "direcionado", "ten_points": "10 pontos", "the_survey_will_be_shown_multiple_times_until_they_respond": "A pesquisa vai ser mostrada várias vezes até eles responderem", diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json index 7acce9c71f..9f0167c466 100644 --- a/apps/web/locales/pt-PT.json +++ b/apps/web/locales/pt-PT.json @@ -1212,6 +1212,7 @@ "add_photo_or_video": "Adicionar foto ou vídeo", "add_pin": "Adicionar PIN", "add_question_below": "Adicionar pergunta abaixo", + "add_question_to_block": "Adicionar pergunta ao bloco", "add_row": "Adicionar linha", "add_variable": "Adicionar variável", "address_fields": "Campos de Endereço", @@ -1243,6 +1244,8 @@ "bold": "Negrito", "brand_color": "Cor da marca", "brightness": "Brilho", + "button_external": "Ativar link externo", + "button_external_description": "Adicionar um botão que abre um URL externo num novo separador", "button_label": "Rótulo do botão", "button_url": "URL do botão", "cal_username": "Nome de utilizador do Cal.com ou nome de utilizador/evento", @@ -1306,6 +1309,7 @@ "create_group": "Criar grupo", "create_your_own_survey": "Crie o seu próprio inquérito", "css_selector": "Seletor CSS", + "cta_button_label": "Etiqueta do botão \"CTA\"", "custom_hostname": "Nome do host personalizado", "darken_or_lighten_background_of_your_choice": "Escurecer ou clarear o fundo da sua escolha.", "date_format": "Formato da data", @@ -1324,9 +1328,11 @@ "does_not_include_one_of": "Não inclui um de", "does_not_start_with": "Não começa com", "duplicate_block": "Duplicar bloco", + "duplicate_question": "Duplicar pergunta", "edit_link": "Editar link", "edit_recall": "Editar Lembrete", "edit_translations": "Editar traduções {lang}", + "element_not_found": "Pergunta não encontrada", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permitir aos participantes mudar a língua do inquérito a qualquer momento durante o inquérito.", "enable_recaptcha_to_protect_your_survey_from_spam": "A proteção contra spam usa o reCAPTCHA v3 para filtrar as respostas de spam.", "enable_spam_protection": "Proteção contra spam", @@ -1402,11 +1408,12 @@ "hidden_field_used_in_recall": "Campo oculto \"{hiddenField}\" está a ser recordado na pergunta {questionIndex}.", "hidden_field_used_in_recall_ending_card": "Campo oculto \"{hiddenField}\" está a ser recordado no Cartão de Conclusão", "hidden_field_used_in_recall_welcome": "Campo oculto \"{hiddenField}\" está a ser recordado no cartão de boas-vindas.", - "hide_advanced_settings": "Ocultar definições avançadas", "hide_back_button": "Ocultar botão 'Retroceder'", "hide_back_button_description": "Não mostrar o botão de retroceder no inquérito", + "hide_block_settings": "Ocultar definições do bloco", "hide_logo": "Esconder logótipo", "hide_progress_bar": "Ocultar barra de progresso", + "hide_question_settings": "Ocultar definições da pergunta", "hide_the_logo_in_this_specific_survey": "Ocultar o logótipo neste inquérito específico", "hostname": "Nome do host", "how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Quão extravagantes quer os seus cartões em Inquéritos {surveyTypeDerived}", @@ -1439,7 +1446,7 @@ "is_skipped": "É ignorado", "is_submitted": "Está submetido", "italic": "Itálico", - "jump_to_question": "Saltar para a pergunta", + "jump_to_block": "Saltar para o bloco", "keep_current_order": "Manter ordem atual", "keep_showing_while_conditions_match": "Continuar a mostrar enquanto as condições corresponderem", "key": "Chave", @@ -1453,16 +1460,18 @@ "logic_error_warning": "A alteração causará erros de lógica", "logic_error_warning_text": "Alterar o tipo de pergunta irá remover as condições lógicas desta pergunta", "long_answer": "Resposta longa", + "long_answer_toggle_description": "Permitir que os inquiridos escrevam respostas mais longas e com várias linhas.", "lower_label": "Etiqueta Inferior", "manage_languages": "Gerir Idiomas", "matrix_all_fields": "Todos os campos", "matrix_rows": "Linhas", "max_file_size": "Tamanho máximo do ficheiro", "max_file_size_limit_is": "O limite do tamanho máximo do ficheiro é", + "move_question_to_block": "Mover pergunta para o bloco", "multiply": "Multiplicar *", "needed_for_self_hosted_cal_com_instance": "Necessário para uma instância auto-hospedada do Cal.com", + "next_block": "Bloco seguinte", "next_button_label": "Rótulo do botão \"Seguinte\"", - "next_question": "Próxima pergunta", "no_hidden_fields_yet_add_first_one_below": "Ainda não há campos ocultos. Adicione o primeiro abaixo.", "no_images_found_for": "Não foram encontradas imagens para ''{query}\"", "no_languages_found_add_first_one_to_get_started": "Nenhuma língua encontrada. Adicione a primeira para começar.", @@ -1570,11 +1579,12 @@ "set_the_global_placement_in_the_look_feel_settings": "Definir a colocação global nas definições de Aparência.", "settings_saved_successfully": "Definições guardadas com sucesso", "seven_points": "7 pontos", - "show_advanced_settings": "Mostrar definições avançadas", + "show_block_settings": "Mostrar definições do bloco", "show_button": "Mostrar Botão", "show_language_switch": "Mostrar alternador de idioma", "show_multiple_times": "Mostrar várias vezes", "show_only_once": "Mostrar apenas uma vez", + "show_question_settings": "Mostrar definições da pergunta", "show_survey_maximum_of": "Mostrar inquérito máximo de", "show_survey_to_users": "Mostrar inquérito a % dos utilizadores", "show_to_x_percentage_of_targeted_users": "Mostrar a {percentage}% dos utilizadores alvo", @@ -1600,6 +1610,7 @@ "survey_placement": "Colocação do Inquérito", "survey_trigger": "Desencadeador de Inquérito", "switch_multi_lanugage_on_to_get_started": "Ative o modo multilingue para começar 👉", + "target_block_not_found": "Bloco de destino não encontrado", "targeted": "Alvo", "ten_points": "10 pontos", "the_survey_will_be_shown_multiple_times_until_they_respond": "O inquérito será mostrado várias vezes até que respondam", diff --git a/apps/web/locales/ro-RO.json b/apps/web/locales/ro-RO.json index 637f7468c1..8e1f912100 100644 --- a/apps/web/locales/ro-RO.json +++ b/apps/web/locales/ro-RO.json @@ -1212,6 +1212,7 @@ "add_photo_or_video": "Adaugă fotografie sau video", "add_pin": "Adaugă PIN", "add_question_below": "Adaugă întrebare mai jos", + "add_question_to_block": "Adaugă întrebare în bloc", "add_row": "Adăugați rând", "add_variable": "Adaugă variabilă", "address_fields": "Câmpuri Adresă", @@ -1243,6 +1244,8 @@ "bold": "Îngroșat", "brand_color": "Culoarea brandului", "brightness": "Luminozitate", + "button_external": "Activează link extern", + "button_external_description": "Adaugă un buton care deschide un URL extern într-o filă nouă", "button_label": "Etichetă buton", "button_url": "URL Buton", "cal_username": "Utilizator Cal.com sau utilizator/eveniment", @@ -1306,6 +1309,7 @@ "create_group": "Creează grup", "create_your_own_survey": "Creează-ți propriul chestionar", "css_selector": "Selector CSS", + "cta_button_label": "Eticheta butonului \"CTA\"", "custom_hostname": "Gazdă personalizată", "darken_or_lighten_background_of_your_choice": "Întunecați sau luminați fundalul după preferințe.", "date_format": "Format dată", @@ -1324,9 +1328,11 @@ "does_not_include_one_of": "Nu include una dintre", "does_not_start_with": "Nu începe cu", "duplicate_block": "Duplicați blocul", + "duplicate_question": "Duplică întrebarea", "edit_link": "Editare legătură", "edit_recall": "Editează Referințele", "edit_translations": "Editează traducerile {lang}", + "element_not_found": "Întrebarea nu a fost găsită", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "Permite participanților să schimbe limba sondajului în orice moment în timpul sondajului.", "enable_recaptcha_to_protect_your_survey_from_spam": "Protecția împotriva spamului folosește reCAPTCHA v3 pentru a filtra răspunsurile de spam.", "enable_spam_protection": "Protecția împotriva spamului", @@ -1402,11 +1408,12 @@ "hidden_field_used_in_recall": "Câmpul ascuns \"{hiddenField}\" este reamintit în întrebarea {questionIndex}.", "hidden_field_used_in_recall_ending_card": "Câmpul ascuns \"{hiddenField}\" este reamintit în Cardul de Încheiere.", "hidden_field_used_in_recall_welcome": "Câmpul ascuns \"{hiddenField}\" este reamintit în cardul de bun venit.", - "hide_advanced_settings": "Ascunde setări avansate", "hide_back_button": "Ascunde butonul 'Înapoi'", "hide_back_button_description": "Nu afișa butonul Înapoi în sondaj", + "hide_block_settings": "Ascunde setările blocului", "hide_logo": "Ascunde logo", "hide_progress_bar": "Ascunde bara de progres", + "hide_question_settings": "Ascunde setările întrebării", "hide_the_logo_in_this_specific_survey": "Ascunde logo-ul în acest chestionar specific", "hostname": "Nume gazdă", "how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "Cât de funky doriți să fie cardurile dumneavoastră în sondajele de tip {surveyTypeDerived}", @@ -1439,7 +1446,7 @@ "is_skipped": "Este sărit", "is_submitted": "Este trimis", "italic": "Cursiv", - "jump_to_question": "Sări la întrebare", + "jump_to_block": "Sari la bloc", "keep_current_order": "Păstrați ordinea actuală", "keep_showing_while_conditions_match": "Continuă să afișezi cât timp condițiile se potrivesc", "key": "Cheie", @@ -1453,16 +1460,18 @@ "logic_error_warning": "Schimbarea va provoca erori de logică", "logic_error_warning_text": "Schimbarea tipului de întrebare va elimina condițiile de logică din această întrebare", "long_answer": "Răspuns lung", + "long_answer_toggle_description": "Permite respondenților să scrie răspunsuri mai lungi, pe mai multe rânduri.", "lower_label": "Etichetă inferioară", "manage_languages": "Gestionați limbile", "matrix_all_fields": "Toate câmpurile", "matrix_rows": "Rânduri", "max_file_size": "Dimensiune maximă fișier", "max_file_size_limit_is": "Limita dimensiunii maxime a fișierului este", + "move_question_to_block": "Mută întrebarea în bloc", "multiply": "Multiplicare", "needed_for_self_hosted_cal_com_instance": "Necesar pentru un exemplu autogăzduit Cal.com", + "next_block": "Blocul următor", "next_button_label": "Etichetă buton \"Următorul\"", - "next_question": "Întrebarea următoare", "no_hidden_fields_yet_add_first_one_below": "Nu există încă câmpuri ascunse. Adăugați primul mai jos.", "no_images_found_for": "Nicio imagine găsită pentru ''{query}\"", "no_languages_found_add_first_one_to_get_started": "Nu s-au găsit limbi. Adaugă prima pentru a începe.", @@ -1570,11 +1579,12 @@ "set_the_global_placement_in_the_look_feel_settings": "Setați amplasarea globală în setările Aspect & Stil.", "settings_saved_successfully": "Setările au fost salvate cu succes.", "seven_points": "7 puncte", - "show_advanced_settings": "Afișați setările avansate", + "show_block_settings": "Afișează setările blocului", "show_button": "Afișează butonul", "show_language_switch": "Afișează comutatorul de limbă", "show_multiple_times": "Afișează de mai multe ori", "show_only_once": "Afișează doar o dată", + "show_question_settings": "Afișează setările întrebării", "show_survey_maximum_of": "Afișează sondajul de maxim", "show_survey_to_users": "Afișați sondajul la % din utilizatori", "show_to_x_percentage_of_targeted_users": "Afișați la {percentage}% din utilizatorii vizați", @@ -1600,6 +1610,7 @@ "survey_placement": "Amplasarea sondajului", "survey_trigger": "Declanșator sondaj", "switch_multi_lanugage_on_to_get_started": "Comutați pe modul multilingv pentru a începe 👉", + "target_block_not_found": "Blocul țintă nu a fost găsit", "targeted": "Ţintite", "ten_points": "10 puncte", "the_survey_will_be_shown_multiple_times_until_they_respond": "Sondajul va fi afișat de mai multe ori până când vor răspunde", diff --git a/apps/web/locales/zh-Hans-CN.json b/apps/web/locales/zh-Hans-CN.json index 56f8b14413..2820d69a93 100644 --- a/apps/web/locales/zh-Hans-CN.json +++ b/apps/web/locales/zh-Hans-CN.json @@ -1212,6 +1212,7 @@ "add_photo_or_video": "添加 照片 或 视频", "add_pin": "添加 PIN", "add_question_below": "在下面 添加 问题", + "add_question_to_block": "添加问题到区块", "add_row": "添加 行", "add_variable": "添加 变量", "address_fields": "地址字段", @@ -1243,6 +1244,8 @@ "bold": "粗体", "brand_color": "品牌 颜色", "brightness": "亮度", + "button_external": "启用外部链接", + "button_external_description": "添加一个按钮,在新标签页中打开外部URL", "button_label": "按钮标签", "button_url": "按钮 URL", "cal_username": "Cal.com 用户名 或 用户名/事件", @@ -1306,6 +1309,7 @@ "create_group": "创建 群组", "create_your_own_survey": "创建 你 的 调查", "css_selector": "CSS 选择器", + "cta_button_label": "“CTA”按钮标签", "custom_hostname": "自 定 义 主 机 名", "darken_or_lighten_background_of_your_choice": "根据 您 的 选择 暗化 或 亮化 背景。", "date_format": "日期格式", @@ -1324,9 +1328,11 @@ "does_not_include_one_of": "不包括一 个", "does_not_start_with": "不 以 开头", "duplicate_block": "复制区块", + "duplicate_question": "复制问题", "edit_link": "编辑 链接", "edit_recall": "编辑 调用", "edit_translations": "编辑 {lang} 翻译", + "element_not_found": "未找到问题", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "启用 参与者 在 调查 过程中 的 任何 时间 点 切换 调查 语言。", "enable_recaptcha_to_protect_your_survey_from_spam": "垃圾 邮件 保护 使用 reCAPTCHA v3 来 过滤 掉 垃圾 响应 。", "enable_spam_protection": "垃圾 邮件 保护", @@ -1402,11 +1408,12 @@ "hidden_field_used_in_recall": "隐藏 字段 \"{hiddenField}\" 正在召回于问题 {questionIndex}。", "hidden_field_used_in_recall_ending_card": "隐藏 字段 \"{hiddenField}\" 正在召回于结束 卡", "hidden_field_used_in_recall_welcome": "隐藏 字段 \"{hiddenField}\" 正在召回于欢迎 卡 。", - "hide_advanced_settings": "隐藏 高级设置", "hide_back_button": "隐藏 \"返回\" 按钮", "hide_back_button_description": "不 显示 调查 中 的 返回 按钮", + "hide_block_settings": "隐藏区块设置", "hide_logo": "隐藏 徽标", "hide_progress_bar": "隐藏 进度 条", + "hide_question_settings": "隐藏问题设置", "hide_the_logo_in_this_specific_survey": "隐藏此特定调查中的 logo", "hostname": "主 机 名", "how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "在 {surveyTypeDerived} 调查 中,您 想要 卡片 多么 有趣", @@ -1439,7 +1446,7 @@ "is_skipped": "已跳过", "is_submitted": "已提交", "italic": "斜体", - "jump_to_question": "跳 转 到 问题", + "jump_to_block": "跳转到区块", "keep_current_order": "保持 当前 顺序", "keep_showing_while_conditions_match": "条件 符合 时 保持 显示", "key": "键", @@ -1453,16 +1460,18 @@ "logic_error_warning": "更改 将 导致 逻辑 错误", "logic_error_warning_text": "更改问题类型 会 移除 此问题 的 逻辑条件", "long_answer": "长答案", + "long_answer_toggle_description": "允许受访者填写较长的多行答案。", "lower_label": "下限标签", "manage_languages": "管理 语言", "matrix_all_fields": "所有字段", "matrix_rows": "行", "max_file_size": "最大 文件 大小", "max_file_size_limit_is": "最大 文件 大小 限制 是", + "move_question_to_block": "将问题移动到区块", "multiply": "乘 *", "needed_for_self_hosted_cal_com_instance": "需要用于 自建 Cal.com 实例", + "next_block": "下一块", "next_button_label": "\"下一步\" 按钮标签", - "next_question": "下一个问题", "no_hidden_fields_yet_add_first_one_below": "还没有隐藏字段。 在下面添加第一个。", "no_images_found_for": "未找到与 \"{query}\" 相关的图片", "no_languages_found_add_first_one_to_get_started": "没有找到语言。添加第一个以开始。", @@ -1570,11 +1579,12 @@ "set_the_global_placement_in_the_look_feel_settings": "在外观 设置中 设置 全局 放置。", "settings_saved_successfully": "设置 保存 成功", "seven_points": "7 分", - "show_advanced_settings": "显示 高级设置", + "show_block_settings": "显示区块设置", "show_button": "显示 按钮", "show_language_switch": "显示 语言 切换", "show_multiple_times": "显示 多次", "show_only_once": "仅 显示 一次", + "show_question_settings": "显示问题设置", "show_survey_maximum_of": "显示 调查 最大 一次", "show_survey_to_users": "显示 问卷 给 % 的 用户", "show_to_x_percentage_of_targeted_users": "显示 给 {percentage}% 的 目标 用户", @@ -1600,6 +1610,7 @@ "survey_placement": "调查 放置", "survey_trigger": "调查 触发", "switch_multi_lanugage_on_to_get_started": "打开多语言以开始 👉", + "target_block_not_found": "未找到目标区块", "targeted": "定位", "ten_points": "10 分", "the_survey_will_be_shown_multiple_times_until_they_respond": "调查 将 显示 多次 直到 他们 回复", diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json index 139c48a5f6..97e133838e 100644 --- a/apps/web/locales/zh-Hant-TW.json +++ b/apps/web/locales/zh-Hant-TW.json @@ -1212,6 +1212,7 @@ "add_photo_or_video": "新增照片或影片", "add_pin": "新增 PIN 碼", "add_question_below": "在下方新增問題", + "add_question_to_block": "新增問題到區塊", "add_row": "新增列", "add_variable": "新增變數", "address_fields": "地址欄位", @@ -1243,6 +1244,8 @@ "bold": "粗體", "brand_color": "品牌顏色", "brightness": "亮度", + "button_external": "啟用外部連結", + "button_external_description": "新增一個按鈕,在新分頁中開啟外部網址", "button_label": "按鈕標籤", "button_url": "按鈕網址", "cal_username": "Cal.com 使用者名稱或使用者名稱/事件", @@ -1306,6 +1309,7 @@ "create_group": "建立群組", "create_your_own_survey": "建立您自己的問卷", "css_selector": "CSS 選取器", + "cta_button_label": "「CTA」按鈕標籤", "custom_hostname": "自訂主機名稱", "darken_or_lighten_background_of_your_choice": "變暗或變亮您選擇的背景。", "date_format": "日期格式", @@ -1324,9 +1328,11 @@ "does_not_include_one_of": "不包含其中之一", "does_not_start_with": "不以...開頭", "duplicate_block": "複製區塊", + "duplicate_question": "複製問題", "edit_link": "編輯 連結", "edit_recall": "編輯回憶", "edit_translations": "編輯 '{'language'}' 翻譯", + "element_not_found": "找不到問題", "enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey": "允許參與者在問卷中的任何時間點切換問卷語言。", "enable_recaptcha_to_protect_your_survey_from_spam": "垃圾郵件保護使用 reCAPTCHA v3 過濾垃圾回應。", "enable_spam_protection": "垃圾郵件保護", @@ -1402,11 +1408,12 @@ "hidden_field_used_in_recall": "隱藏欄位 \"{hiddenField}\" 於問題 {questionIndex} 中被召回。", "hidden_field_used_in_recall_ending_card": "隱藏欄位 \"{hiddenField}\" 於結束卡中被召回。", "hidden_field_used_in_recall_welcome": "隱藏欄位 \"{hiddenField}\" 於歡迎卡中被召回。", - "hide_advanced_settings": "隱藏進階設定", "hide_back_button": "隱藏「Back」按鈕", "hide_back_button_description": "不要在問卷中顯示返回按鈕", + "hide_block_settings": "隱藏區塊設定", "hide_logo": "隱藏標誌", "hide_progress_bar": "隱藏進度列", + "hide_question_settings": "隱藏問題設定", "hide_the_logo_in_this_specific_survey": "在此特定問卷中隱藏標誌", "hostname": "主機名稱", "how_funky_do_you_want_your_cards_in_survey_type_derived_surveys": "您希望 '{'surveyTypeDerived'}' 問卷中的卡片有多酷炫", @@ -1439,7 +1446,7 @@ "is_skipped": "已跳過", "is_submitted": "已提交", "italic": "斜體", - "jump_to_question": "跳至問題", + "jump_to_block": "跳至區塊", "keep_current_order": "保留目前順序", "keep_showing_while_conditions_match": "在條件符合時持續顯示", "key": "金鑰", @@ -1453,16 +1460,18 @@ "logic_error_warning": "變更將導致邏輯錯誤", "logic_error_warning_text": "變更問題類型將會從此問題中移除邏輯條件", "long_answer": "長回答", + "long_answer_toggle_description": "允許受訪者撰寫較長的多行回答。", "lower_label": "下標籤", "manage_languages": "管理語言", "matrix_all_fields": "所有欄位", "matrix_rows": "列", "max_file_size": "最大檔案大小", "max_file_size_limit_is": "最大檔案大小限制為", + "move_question_to_block": "將問題移至區塊", "multiply": "乘 *", "needed_for_self_hosted_cal_com_instance": "自行託管 Cal.com 執行個體時需要", + "next_block": "下一個區塊", "next_button_label": "「下一步」按鈕標籤", - "next_question": "下一個問題", "no_hidden_fields_yet_add_first_one_below": "尚無隱藏欄位。在下方新增第一個隱藏欄位。", "no_images_found_for": "找不到「'{'query'}'」的圖片", "no_languages_found_add_first_one_to_get_started": "找不到語言。新增第一個語言以開始使用。", @@ -1570,11 +1579,12 @@ "set_the_global_placement_in_the_look_feel_settings": "在「外觀與風格」設定中設定整體位置。", "settings_saved_successfully": "設定已成功儲存", "seven_points": "7 分", - "show_advanced_settings": "顯示進階設定", + "show_block_settings": "顯示區塊設定", "show_button": "顯示按鈕", "show_language_switch": "顯示語言切換", "show_multiple_times": "多次顯示", "show_only_once": "僅顯示一次", + "show_question_settings": "顯示問題設定", "show_survey_maximum_of": "最多顯示問卷", "show_survey_to_users": "將問卷顯示給 % 的使用者", "show_to_x_percentage_of_targeted_users": "顯示給 '{'percentage'}'% 的目標使用者", @@ -1600,6 +1610,7 @@ "survey_placement": "問卷位置", "survey_trigger": "問卷觸發器", "switch_multi_lanugage_on_to_get_started": "開啟多語言以開始使用 👉", + "target_block_not_found": "找不到目標區塊", "targeted": "目標", "ten_points": "10 分", "the_survey_will_be_shown_multiple_times_until_they_respond": "將多次顯示問卷,直到他們回應", diff --git a/apps/web/modules/ee/quotas/components/quota-modal.tsx b/apps/web/modules/ee/quotas/components/quota-modal.tsx index 3c73fd4496..018c44e931 100644 --- a/apps/web/modules/ee/quotas/components/quota-modal.tsx +++ b/apps/web/modules/ee/quotas/components/quota-modal.tsx @@ -20,7 +20,7 @@ import { TSurvey } from "@formbricks/types/surveys/types"; import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { createQuotaAction, updateQuotaAction } from "@/modules/ee/quotas/actions"; import { EndingCardSelector } from "@/modules/ee/quotas/components/ending-card-selector"; -import { getDefaultOperatorForQuestion } from "@/modules/survey/editor/lib/utils"; +import { getDefaultOperatorForElement } from "@/modules/survey/editor/lib/utils"; import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils"; import { Button } from "@/modules/ui/components/button"; import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal"; @@ -95,7 +95,7 @@ export const QuotaModal = ({ { id: createId(), leftOperand: { type: "question", value: firstQuestion?.id }, - operator: firstQuestion ? getDefaultOperatorForQuestion(firstQuestion, t) : "equals", + operator: firstQuestion ? getDefaultOperatorForElement(firstQuestion, t) : "equals", }, ], }, diff --git a/apps/web/modules/survey/components/question-form-input/index.tsx b/apps/web/modules/survey/components/question-form-input/index.tsx index a17b3a469f..9717770a44 100644 --- a/apps/web/modules/survey/components/question-form-input/index.tsx +++ b/apps/web/modules/survey/components/question-form-input/index.tsx @@ -24,6 +24,7 @@ import { Button } from "@/modules/ui/components/button"; import { FileInput } from "@/modules/ui/components/file-input"; import { Input } from "@/modules/ui/components/input"; import { Label } from "@/modules/ui/components/label"; +import { Switch } from "@/modules/ui/components/switch"; import { TooltipRenderer } from "@/modules/ui/components/tooltip"; import { determineImageUploaderVisibility, @@ -309,6 +310,60 @@ export const QuestionFormInput = ({ return false; }; + const getIsRequiredToggleDisabled = (): boolean => { + if (!question) return false; + + if (question.type === TSurveyElementTypeEnum.Address) { + const allFieldsAreOptional = [ + question.addressLine1, + question.addressLine2, + question.city, + question.state, + question.zip, + question.country, + ] + .filter((field) => field.show) + .every((field) => !field.required); + + if (allFieldsAreOptional) { + return true; + } + + return [ + question.addressLine1, + question.addressLine2, + question.city, + question.state, + question.zip, + question.country, + ] + .filter((field) => field.show) + .some((condition) => condition.required === true); + } + + if (question.type === TSurveyElementTypeEnum.ContactInfo) { + const allFieldsAreOptional = [ + question.firstName, + question.lastName, + question.email, + question.phone, + question.company, + ] + .filter((field) => field.show) + .every((field) => !field.required); + + if (allFieldsAreOptional) { + return true; + } + + return [question.firstName, question.lastName, question.email, question.phone, question.company] + .filter((field) => field.show) + .some((condition) => condition.required === true); + } + + return false; + }; + const useRichTextEditor = id === "headline" || id === "subheader" || id === "html"; // For rich text editor fields, we need either updateQuestion or updateSurvey @@ -320,8 +375,23 @@ export const QuestionFormInput = ({ return (
{label && ( -
+
+ {id === "headline" && question && updateQuestion && ( +
+ + { + updateQuestion(questionIdx, { required: checked }); + }} + /> +
+ )}
)}
@@ -375,7 +445,9 @@ export const QuestionFormInput = ({
{id === "headline" && !isWelcomeCard && ( - + diff --git a/apps/web/modules/survey/editor/components/advanced-settings.tsx b/apps/web/modules/survey/editor/components/advanced-settings.tsx index 69180984e8..fa2012f95a 100644 --- a/apps/web/modules/survey/editor/components/advanced-settings.tsx +++ b/apps/web/modules/survey/editor/components/advanced-settings.tsx @@ -1,7 +1,6 @@ import { TSurveyBlockLogic } from "@formbricks/types/surveys/blocks"; import { TSurveyElement, TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements"; import { TSurvey } from "@formbricks/types/surveys/types"; -import { ConditionalLogic } from "@/modules/survey/editor/components/conditional-logic"; import { OptionIds } from "@/modules/survey/editor/components/option-ids"; import { UpdateQuestionId } from "@/modules/survey/editor/components/update-question-id"; @@ -20,8 +19,6 @@ export const AdvancedSettings = ({ questionIdx, localSurvey, updateQuestion, - updateBlockLogic, - updateBlockLogicFallback, selectedLanguageCode, }: AdvancedSettingsProps) => { const showOptionIds = @@ -32,16 +29,6 @@ export const AdvancedSettings = ({ return (
- {/* TODO: Re-enable ConditionalLogic in post-MVP */} - {/* */} - void; moveBlock: (blockId: string, direction: "up" | "down") => void; addElementToBlock: (element: TSurveyElement, blockId: string, afterElementIdx: number) => void; + moveElementToBlock?: (elementId: string, targetBlockId: string) => void; totalBlocks: number; } @@ -112,6 +112,7 @@ export const BlockCard = ({ deleteBlock, moveBlock, addElementToBlock, + moveElementToBlock, totalBlocks, }: BlockCardProps) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ @@ -120,35 +121,35 @@ export const BlockCard = ({ const { t } = useTranslation(); const QUESTIONS_ICON_MAP = getQuestionIconMap(t); - // Block-level properties - const blockName = block.name || `Block ${blockIdx + 1}`; const hasMultipleElements = block.elements.length > 1; const blockLogic = block.logic ?? []; // Check if any element in this block is currently active const isBlockOpen = block.elements.some((element) => element.id === activeQuestionId); + const hasInvalidElement = block.elements.some((element) => invalidQuestions?.includes(element.id)); + + // Check if button labels have incomplete translations for any enabled language + // A button label is invalid if it exists but doesn't have valid text for all enabled languages + const hasInvalidButtonLabel = + block.buttonLabel !== undefined && + !isLabelValidForAllLanguages(block.buttonLabel, localSurvey.languages ?? []); + + // Check if back button label is invalid + // Back button label should exist for all blocks except the first one + const hasInvalidBackButtonLabel = + blockIdx > 0 && + block.backButtonLabel !== undefined && + !isLabelValidForAllLanguages(block.backButtonLabel, localSurvey.languages ?? []); + + // Block should be highlighted if it has invalid elements OR invalid button labels + const isBlockInvalid = hasInvalidElement || hasInvalidButtonLabel || hasInvalidBackButtonLabel; + + const [isBlockCollapsed, setIsBlockCollapsed] = useState(false); const [openAdvanced, setOpenAdvanced] = useState(blockLogic.length > 0); + const [parent] = useAutoAnimate(); - - // Get button labels from the block - const blockButtonLabel = block.buttonLabel; - const blockBackButtonLabel = block.backButtonLabel; - - const updateEmptyButtonLabels = ( - labelKey: "buttonLabel" | "backButtonLabel", - labelValue: TI18nString, - skipBlockIndex: number - ) => { - // Update button labels for all blocks except the one at skipBlockIndex - localSurvey.blocks.forEach((block, index) => { - if (index === skipBlockIndex) return; - const currentLabel = block[labelKey]; - if (!currentLabel || currentLabel[selectedLanguageCode]?.trim() === "") { - updateBlockButtonLabel(index, labelKey, labelValue); - } - }); - }; + const [elementsParent] = useAutoAnimate(); const getElementHeadline = ( element: TSurveyElement, @@ -177,11 +178,7 @@ export const BlockCard = ({ ); }; - const renderElementForm = ( - element: TSurveyElement, - questionIdx: number, - blockButtonLabel?: TI18nString - ) => { + const renderElementForm = (element: TSurveyElement, questionIdx: number) => { switch (element.type) { case TSurveyElementTypeEnum.OpenText: return ( @@ -236,14 +233,12 @@ export const BlockCard = ({ question={element} questionIdx={questionIdx} updateQuestion={updateQuestion} - lastQuestion={lastQuestion} selectedLanguageCode={selectedLanguageCode} setSelectedLanguageCode={setSelectedLanguageCode} isInvalid={invalidQuestions ? invalidQuestions.includes(element.id) : false} locale={locale} isStorageConfigured={isStorageConfigured} isExternalUrlsAllowed={isExternalUrlsAllowed} - buttonLabel={blockButtonLabel} /> ); case TSurveyElementTypeEnum.CTA: @@ -427,6 +422,16 @@ export const BlockCard = ({ zIndex: isDragging ? 10 : 1, }; + const blockQuestionCount = block.elements.length; + const blockQuestionCountText = blockQuestionCount === 1 ? "question" : "questions"; + + let blockSidebarColorClass = ""; + if (isBlockInvalid) { + blockSidebarColorClass = "bg-red-400"; + } else { + blockSidebarColorClass = isBlockOpen ? "bg-slate-700" : "bg-slate-400"; + } + return (
-
{blockIdx + 1}
+
+ {blockIdx + 1} +
-
- {/* Block header - shown when block has multiple elements */} - {hasMultipleElements && ( -
-
-

{blockName}

-

{block.elements.length} questions

+
+ setIsBlockCollapsed(!isBlockCollapsed)} + className={cn(isBlockCollapsed ? "h-full" : "")}> + +
+
+
+

{block.name}

+

+ {blockQuestionCount} {blockQuestionCountText} +

+
+
+
+ duplicateBlock(block.id)} + onDelete={() => deleteBlock(block.id)} + onMoveUp={() => moveBlock(block.id, "up")} + onMoveDown={() => moveBlock(block.id, "down")} + /> +
- duplicateBlock(block.id)} - onDelete={() => deleteBlock(block.id)} - onMoveUp={() => moveBlock(block.id, "up")} - onMoveDown={() => moveBlock(block.id, "down")} - /> -
- )} + - {/* Render each element in the block */} - {block.elements.map((element, elementIndex) => { - // Calculate the actual question index in the flattened questions array - let questionIdx = 0; - for (let i = 0; i < blockIdx; i++) { - questionIdx += localSurvey.blocks[i].elements.length; - } - questionIdx += elementIndex; + + {/* Render each element in the block */} +
+ {block.elements.map((element, elementIndex) => { + // Calculate the actual question index in the flattened questions array + let questionIdx = 0; + for (let i = 0; i < blockIdx; i++) { + questionIdx += localSurvey.blocks[i].elements.length; + } + questionIdx += elementIndex; - const isInvalid = invalidQuestions ? invalidQuestions.includes(element.id) : false; - const open = activeQuestionId === element.id; + const isOpen = activeQuestionId === element.id; - const getIsRequiredToggleDisabled = (): boolean => { - if (element.type === TSurveyElementTypeEnum.Address) { - const allFieldsAreOptional = [ - element.addressLine1, - element.addressLine2, - element.city, - element.state, - element.zip, - element.country, - ] - .filter((field) => field.show) - .every((field) => !field.required); - - if (allFieldsAreOptional) { - return true; - } - - return [ - element.addressLine1, - element.addressLine2, - element.city, - element.state, - element.zip, - element.country, - ] - .filter((field) => field.show) - .some((condition) => condition.required === true); - } - - if (element.type === TSurveyElementTypeEnum.ContactInfo) { - const allFieldsAreOptional = [ - element.firstName, - element.lastName, - element.email, - element.phone, - element.company, - ] - .filter((field) => field.show) - .every((field) => !field.required); - - if (allFieldsAreOptional) { - return true; - } - - return [element.firstName, element.lastName, element.email, element.phone, element.company] - .filter((field) => field.show) - .some((condition) => condition.required === true); - } - - return false; - }; - - const handleRequiredToggle = () => { - // Fix for NPS and Rating element having missing translations when buttonLabel is not removed - if (!element.required && (element.type === "nps" || element.type === "rating")) { - // Remove buttonLabel from the block when making NPS/Rating required - updateBlockButtonLabel(blockIdx, "buttonLabel", undefined); - updateQuestion(questionIdx, { required: true }); - } else { - updateQuestion(questionIdx, { required: !element.required }); - } - }; - - return ( -
0 && "border-t border-slate-200")}> - { - if (activeQuestionId !== element.id) { - setActiveQuestionId(element.id); - } else { - setActiveQuestionId(null); - } - }} - className="w-full"> - -
-
-
-
- {QUESTIONS_ICON_MAP[element.type]} -
-
- {hasMultipleElements && ( -

- Question {elementIndex + 1} -

- )} -

- {getElementHeadline(element, selectedLanguageCode)} -

- {!open && ( -

- {element?.required - ? t("environments.surveys.edit.required") - : t("environments.surveys.edit.optional")} -

- )} -
-
-
- -
- -
-
-
- - {shouldShowCautionAlert(element.type) && ( - - {t("environments.surveys.edit.caution_text")} - onAlertTrigger()}>{t("common.learn_more")} - - )} - - {renderElementForm(element, questionIdx, blockButtonLabel)} -
- + return ( +
0 && "border-t border-slate-200")}> + { + if (activeQuestionId !== element.id) { + setActiveQuestionId(element.id); + } else { + setActiveQuestionId(null); + } + }} + className="w-full"> - {openAdvanced ? ( - - ) : ( - + asChild + className={cn( + isOpen ? "bg-slate-50" : "", + "flex w-full cursor-pointer justify-between gap-4 p-4 hover:bg-slate-50" )} - {openAdvanced - ? t("environments.surveys.edit.hide_advanced_settings") - : t("environments.surveys.edit.show_advanced_settings")} - - - - {element.type !== TSurveyElementTypeEnum.NPS && - element.type !== TSurveyElementTypeEnum.Rating && - element.type !== TSurveyElementTypeEnum.CTA ? ( -
- {questionIdx !== 0 && ( - { - if (!blockBackButtonLabel) return; - let translatedBackButtonLabel = { - ...blockBackButtonLabel, - [selectedLanguageCode]: e.target.value, - }; - updateBlockButtonLabel( - blockIdx, - "backButtonLabel", - translatedBackButtonLabel - ); - updateEmptyButtonLabels( - "backButtonLabel", - translatedBackButtonLabel, - blockIdx - ); - }} - isStorageConfigured={isStorageConfigured} - /> - )} -
- { - if (!blockButtonLabel) return; - let translatedNextButtonLabel = { - ...blockButtonLabel, - [selectedLanguageCode]: e.target.value, - }; - updateBlockButtonLabel(blockIdx, "buttonLabel", translatedNextButtonLabel); - // Don't propagate to last block - const lastBlockIndex = localSurvey.blocks.length - 1; - if (blockIdx !== lastBlockIndex) { - updateEmptyButtonLabels( - "buttonLabel", - translatedNextButtonLabel, - lastBlockIndex - ); - } - }} - locale={locale} - isStorageConfigured={isStorageConfigured} - /> + aria-label="Toggle question details"> +
+
+
+
+ {QUESTIONS_ICON_MAP[element.type]} +
+
+ {hasMultipleElements && ( +

+ Question {elementIndex + 1} +

+ )} +

+ {getElementHeadline(element, selectedLanguageCode)} +

+ {!isOpen && ( +

+ {element?.required + ? t("environments.surveys.edit.required") + : t("environments.surveys.edit.optional")} +

+ )} +
- ) : null} - {(element.type === TSurveyElementTypeEnum.Rating || - element.type === TSurveyElementTypeEnum.NPS) && - questionIdx !== 0 && ( -
- { - if (!blockBackButtonLabel) return; - const translatedBackButtonLabel = { - ...blockBackButtonLabel, - [selectedLanguageCode]: e.target.value, - }; - updateBlockButtonLabel( - blockIdx, - "backButtonLabel", - translatedBackButtonLabel - ); - updateEmptyButtonLabels( - "backButtonLabel", - translatedBackButtonLabel, - blockIdx - ); - }} - isStorageConfigured={isStorageConfigured} - /> -
- )} - +
+ +
+
+ + + {shouldShowCautionAlert(element.type) && ( + + {t("environments.surveys.edit.caution_text")} + onAlertTrigger()}> + {t("common.learn_more")} + + + )} + {renderElementForm(element, questionIdx)} +
+ + + {openAdvanced ? ( + + ) : ( + + )} + {openAdvanced + ? t("environments.surveys.edit.hide_question_settings") + : t("environments.surveys.edit.show_question_settings")} + + + + {element.type !== TSurveyElementTypeEnum.NPS && + element.type !== TSurveyElementTypeEnum.Rating && + element.type !== TSurveyElementTypeEnum.CTA ? ( +
+ ) : null} + +
+
+
- - - {open && ( -
- {element.type === "openText" && ( -
- - { - e.stopPropagation(); - updateQuestion(questionIdx, { - longAnswer: - typeof element.longAnswer === "undefined" ? false : !element.longAnswer, - }); - }} - /> -
- )} - { -
- - { - e.stopPropagation(); - handleRequiredToggle(); - }} - /> -
- } -
- )} - + ); + })}
- ); - })} +
+ {/* Add Question to Block button */} - {/* Add Question to Block button */} +
+ +
-
- -
+
+ + {/* Block Settings */} +
+ +
+
+
); diff --git a/apps/web/modules/survey/editor/components/block-menu.tsx b/apps/web/modules/survey/editor/components/block-menu.tsx index 0a1ccdd215..8cc6e00395 100644 --- a/apps/web/modules/survey/editor/components/block-menu.tsx +++ b/apps/web/modules/survey/editor/components/block-menu.tsx @@ -6,9 +6,9 @@ import { Button } from "@/modules/ui/components/button"; import { TooltipRenderer } from "@/modules/ui/components/tooltip"; interface BlockMenuProps { - blockIndex: number; isFirstBlock: boolean; isLastBlock: boolean; + isOnlyBlock: boolean; onDuplicate: () => void; onDelete: () => void; onMoveUp: () => void; @@ -16,9 +16,9 @@ interface BlockMenuProps { } export const BlockMenu = ({ - blockIndex, isFirstBlock, isLastBlock, + isOnlyBlock, onDuplicate, onDelete, onMoveUp, @@ -77,9 +77,12 @@ export const BlockMenu = ({ - + )}
-
+
updateQuestion(questionIdx, { allowMultipleFiles: !question.allowMultipleFiles })} diff --git a/apps/web/modules/survey/editor/components/logic-editor-actions.tsx b/apps/web/modules/survey/editor/components/logic-editor-actions.tsx index 02115b60ed..f52a8dc5e7 100644 --- a/apps/web/modules/survey/editor/components/logic-editor-actions.tsx +++ b/apps/web/modules/survey/editor/components/logic-editor-actions.tsx @@ -4,11 +4,11 @@ import { createId } from "@paralleldrive/cuid2"; import { CopyIcon, CornerDownRightIcon, EllipsisVerticalIcon, PlusIcon, TrashIcon } from "lucide-react"; import { useTranslation } from "react-i18next"; import { + TSurveyBlock, TSurveyBlockLogic, TSurveyBlockLogicAction, TSurveyBlockLogicActionObjective, } from "@formbricks/types/surveys/blocks"; -import { TSurveyElement } from "@formbricks/types/surveys/elements"; import { TActionNumberVariableCalculateOperator, TActionTextVariableCalculateOperator, @@ -33,32 +33,29 @@ import { import { InputCombobox } from "@/modules/ui/components/input-combo-box"; import { cn } from "@/modules/ui/lib/utils"; -interface LogicEditorActions { +interface LogicEditorActionsProps { localSurvey: TSurvey; logicItem: TSurveyBlockLogic; logicIdx: number; - question: TSurveyElement; - updateQuestion: (questionIdx: number, updatedAttributes: any) => void; - updateBlockLogic: (questionIdx: number, logic: TSurveyBlockLogic[]) => void; - questionIdx: number; + block: TSurveyBlock; + updateBlockLogic: (blockIdx: number, logic: TSurveyBlockLogic[]) => void; + blockIdx: number; + isLast?: boolean; } export function LogicEditorActions({ localSurvey, logicItem, logicIdx, - question, + block, updateBlockLogic, - questionIdx, -}: LogicEditorActions) { + blockIdx, + isLast, +}: LogicEditorActionsProps) { const actions = logicItem.actions; const { t } = useTranslation(); - // Find the parent block for this question/element to get its logic - const parentBlock = localSurvey.blocks?.find((block) => - block.elements.some((element) => element.id === question.id) - ); - const blockLogic = parentBlock?.logic ?? []; + const blockLogic = block.logic ?? []; const handleActionsChange = ( operation: "remove" | "addBelow" | "duplicate" | "update", @@ -89,7 +86,7 @@ export function LogicEditorActions({ break; } - updateBlockLogic(questionIdx, logicCopy); + updateBlockLogic(blockIdx, logicCopy); }; const handleObjectiveChange = (actionIdx: number, objective: TSurveyBlockLogicActionObjective) => { @@ -116,7 +113,7 @@ export function LogicEditorActions({ {t("environments.surveys.edit.then")}
-
+
{actions?.map((action, idx) => (
@@ -152,7 +149,7 @@ export function LogicEditorActions({ id={`action-${idx}-target`} key={`target-${action.id}`} showSearch={false} - options={getActionTargetOptions(action, localSurvey, questionIdx, t)} + options={getActionTargetOptions(action, localSurvey, blockIdx, t)} value={action.target} onChangeValue={(val: string) => { handleValuesChange(idx, { @@ -219,7 +216,7 @@ export function LogicEditorActions({ placeholder: "Value", type: localSurvey.variables.find((v) => v.id === action.variableId)?.type || "text", }} - groupedOptions={getActionValueOptions(action.variableId, localSurvey, questionIdx, t)} + groupedOptions={getActionValueOptions(action.variableId, localSurvey, blockIdx, t)} onChangeValue={(val, option, fromInput) => { const fieldType = option?.meta?.type as TActionVariableValueType; diff --git a/apps/web/modules/survey/editor/components/logic-editor-conditions.tsx b/apps/web/modules/survey/editor/components/logic-editor-conditions.tsx index 5088281216..514727ac1a 100644 --- a/apps/web/modules/survey/editor/components/logic-editor-conditions.tsx +++ b/apps/web/modules/survey/editor/components/logic-editor-conditions.tsx @@ -1,21 +1,19 @@ "use client"; import { useTranslation } from "react-i18next"; -import { TSurveyBlockLogic } from "@formbricks/types/surveys/blocks"; -import { TSurveyElement } from "@formbricks/types/surveys/elements"; +import { TSurveyBlock, TSurveyBlockLogic } from "@formbricks/types/surveys/blocks"; import { TConditionGroup } from "@formbricks/types/surveys/logic"; -import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types"; +import { TSurvey } from "@formbricks/types/surveys/types"; import { createSharedConditionsFactory } from "@/modules/survey/editor/lib/shared-conditions-factory"; -import { getDefaultOperatorForQuestion } from "@/modules/survey/editor/lib/utils"; +import { getDefaultOperatorForElement } from "@/modules/survey/editor/lib/utils"; import { ConditionsEditor } from "@/modules/ui/components/conditions-editor"; interface LogicEditorConditionsProps { conditions: TConditionGroup; - updateQuestion: (questionIdx: number, updatedAttributes: Partial) => void; - updateBlockLogic: (questionIdx: number, logic: TSurveyBlockLogic[]) => void; - question: TSurveyElement; + updateBlockLogic: (blockIdx: number, logic: TSurveyBlockLogic[]) => void; + block: TSurveyBlock; localSurvey: TSurvey; - questionIdx: number; + blockIdx: number; logicIdx: number; depth?: number; } @@ -23,26 +21,23 @@ interface LogicEditorConditionsProps { export function LogicEditorConditions({ conditions, logicIdx, - question, + block, localSurvey, - questionIdx, + blockIdx, updateBlockLogic, depth = 0, }: LogicEditorConditionsProps) { const { t } = useTranslation(); - // Find the parent block for this question/element to get its logic - const parentBlock = localSurvey.blocks?.find((block) => - block.elements.some((element) => element.id === question.id) - ); - const blockLogic = parentBlock?.logic ?? []; + const blockLogic = block.logic ?? []; + const firstElement = block.elements[0]; const { config, callbacks } = createSharedConditionsFactory( { survey: localSurvey, t, - questionIdx, - getDefaultOperator: () => getDefaultOperatorForQuestion(question, t), + blockIdx, + getDefaultOperator: () => (firstElement ? getDefaultOperatorForElement(firstElement, t) : "equals"), includeCreateGroup: true, }, { @@ -56,7 +51,7 @@ export function LogicEditorConditions({ logicCopy.splice(logicIdx, 1); } - updateBlockLogic(questionIdx, logicCopy); + updateBlockLogic(blockIdx, logicCopy); }, } ); diff --git a/apps/web/modules/survey/editor/components/logic-editor.tsx b/apps/web/modules/survey/editor/components/logic-editor.tsx index 98c66912d6..9c74711791 100644 --- a/apps/web/modules/survey/editor/components/logic-editor.tsx +++ b/apps/web/modules/survey/editor/components/logic-editor.tsx @@ -1,17 +1,14 @@ "use client"; import { ArrowRightIcon } from "lucide-react"; -import { ReactElement, useMemo } from "react"; +import { useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { TSurveyBlockLogic } from "@formbricks/types/surveys/blocks"; -import { TSurveyElement } from "@formbricks/types/surveys/elements"; +import { TSurveyBlock, TSurveyBlockLogic } from "@formbricks/types/surveys/blocks"; import { TSurvey } from "@formbricks/types/surveys/types"; import { getTextContent } from "@formbricks/types/surveys/validation"; import { recallToHeadline } from "@/lib/utils/recall"; import { LogicEditorActions } from "@/modules/survey/editor/components/logic-editor-actions"; import { LogicEditorConditions } from "@/modules/survey/editor/components/logic-editor-conditions"; -import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils"; -import { getQuestionIconMap } from "@/modules/survey/lib/questions"; import { Select, SelectContent, @@ -23,11 +20,10 @@ import { interface LogicEditorProps { localSurvey: TSurvey; logicItem: TSurveyBlockLogic; - updateQuestion: (questionIdx: number, updatedAttributes: any) => void; - updateBlockLogic: (questionIdx: number, logic: TSurveyBlockLogic[]) => void; - updateBlockLogicFallback: (questionIdx: number, logicFallback: string | undefined) => void; - question: TSurveyElement; - questionIdx: number; + updateBlockLogic: (blockIdx: number, logic: TSurveyBlockLogic[]) => void; + updateBlockLogicFallback: (blockIdx: number, logicFallback: string | undefined) => void; + block: TSurveyBlock; + blockIdx: number; logicIdx: number; isLast: boolean; } @@ -35,58 +31,32 @@ interface LogicEditorProps { export function LogicEditor({ localSurvey, logicItem, - updateQuestion, updateBlockLogic, updateBlockLogicFallback, - question, - questionIdx, + block, + blockIdx, logicIdx, isLast, }: LogicEditorProps) { const { t } = useTranslation(); - const QUESTIONS_ICON_MAP = getQuestionIconMap(t); - // Find the parent block for this question/element to get its logicFallback - const parentBlock = localSurvey.blocks?.find((block) => - block.elements.some((element) => element.id === question.id) - ); - - const blockLogicFallback = parentBlock?.logicFallback; + const blockLogicFallback = block.logicFallback; const fallbackOptions = useMemo(() => { let options: { - icon?: ReactElement; label: string; value: string; }[] = []; - // Derive questions from blocks - const allQuestions = getElementsFromBlocks(localSurvey.blocks); const blocks = localSurvey.blocks; - // Track which blocks we've already added to avoid duplicates when a block has multiple elements - const addedBlockIds = new Set(); + // Add blocks AFTER the current block + for (let i = blockIdx + 1; i < blocks.length; i++) { + const currentBlock = blocks[i]; - // Iterate over the questions AFTER the current question - for (let i = questionIdx + 1; i < allQuestions.length; i++) { - const ques = allQuestions[i]; - const block = blocks.find((b) => b.elements.some((e) => e.id === ques.id)); - - if (!block) continue; - - // Skip if we've already added this block - if (addedBlockIds.has(block.id)) continue; - - addedBlockIds.add(block.id); - - // Use the first element's headline as the block label - const firstElement = block.elements[0]; options.push({ - icon: QUESTIONS_ICON_MAP[firstElement.type], - label: getTextContent( - recallToHeadline(firstElement.headline, localSurvey, false, "default").default ?? "" - ), - value: block.id, + label: currentBlock.name, + value: currentBlock.id, }); } @@ -104,27 +74,26 @@ export function LogicEditor({ }); return options; - }, [localSurvey, questionIdx, QUESTIONS_ICON_MAP, t]); + }, [localSurvey, blockIdx, t]); return (
{isLast ? ( @@ -139,22 +108,19 @@ export function LogicEditor({ autoComplete="true" defaultValue={blockLogicFallback || "defaultSelection"} onValueChange={(val) => { - updateBlockLogicFallback(questionIdx, val === "defaultSelection" ? undefined : val); + updateBlockLogicFallback(blockIdx, val === "defaultSelection" ? undefined : val); }}> - {t("environments.surveys.edit.next_question")} + {t("environments.surveys.edit.next_block")} {fallbackOptions.map((option) => ( -
- {option.icon} - {option.label} -
+ {option.label}
))}
diff --git a/apps/web/modules/survey/editor/components/nps-question-form.tsx b/apps/web/modules/survey/editor/components/nps-question-form.tsx index 4054f23d59..c243f3354e 100644 --- a/apps/web/modules/survey/editor/components/nps-question-form.tsx +++ b/apps/web/modules/survey/editor/components/nps-question-form.tsx @@ -4,7 +4,6 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import { PlusIcon } from "lucide-react"; import { type JSX } from "react"; import { useTranslation } from "react-i18next"; -import { TI18nString } from "@formbricks/types/i18n"; import { TSurveyNPSElement } from "@formbricks/types/surveys/elements"; import { TSurvey } from "@formbricks/types/surveys/types"; import { TUserLocale } from "@formbricks/types/user"; @@ -18,21 +17,18 @@ interface NPSQuestionFormProps { question: TSurveyNPSElement; questionIdx: number; updateQuestion: (questionIdx: number, updatedAttributes: Partial) => void; - lastQuestion: boolean; selectedLanguageCode: string; setSelectedLanguageCode: (languageCode: string) => void; isInvalid: boolean; locale: TUserLocale; isStorageConfigured: boolean; isExternalUrlsAllowed?: boolean; - buttonLabel?: TI18nString; } export const NPSQuestionForm = ({ question, questionIdx, updateQuestion, - lastQuestion, isInvalid, localSurvey, selectedLanguageCode, @@ -40,7 +36,6 @@ export const NPSQuestionForm = ({ locale, isStorageConfigured = true, isExternalUrlsAllowed, - buttonLabel, }: NPSQuestionFormProps): JSX.Element => { const { t } = useTranslation(); const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages); @@ -136,26 +131,6 @@ export const NPSQuestionForm = ({
- {!question.required && ( -
- -
- )} - updateQuestion(questionIdx, { isColorCodingEnabled: !question.isColorCodingEnabled })} diff --git a/apps/web/modules/survey/editor/components/open-question-form.tsx b/apps/web/modules/survey/editor/components/open-question-form.tsx index f0008a7e0e..a467681acf 100644 --- a/apps/web/modules/survey/editor/components/open-question-form.tsx +++ b/apps/web/modules/survey/editor/components/open-question-form.tsx @@ -167,7 +167,7 @@ export const OpenQuestionForm = ({ />
-
+
{showCharLimits && ( )} +
+ { + updateQuestion(questionIdx, { + longAnswer: checked, + }); + }} + htmlId={`longAnswer-${question.id}`} + title={t("environments.surveys.edit.long_answer")} + description={t("environments.surveys.edit.long_answer_toggle_description")} + disabled={question.inputType !== "text"} + customContainerClass="p-0" + /> +
); diff --git a/apps/web/modules/survey/editor/components/questions-view.tsx b/apps/web/modules/survey/editor/components/questions-view.tsx index 97f20313a8..3fb3a918a8 100644 --- a/apps/web/modules/survey/editor/components/questions-view.tsx +++ b/apps/web/modules/survey/editor/components/questions-view.tsx @@ -351,17 +351,13 @@ export const QuestionsView = ({ }; // Update block logic (block-level property) - const updateBlockLogic = (questionIdx: number, logic: TSurveyBlockLogic[]) => { - const question = questions[questionIdx]; - if (!question) return; - - const { blockIndex } = findElementLocation(localSurvey, question.id); - if (blockIndex === -1) return; + const updateBlockLogic = (blockIdx: number, logic: TSurveyBlockLogic[]) => { + if (blockIdx < 0 || blockIdx >= localSurvey.blocks.length) return; setLocalSurvey((prevSurvey) => { const blocks = [...(prevSurvey.blocks ?? [])]; - blocks[blockIndex] = { - ...blocks[blockIndex], + blocks[blockIdx] = { + ...blocks[blockIdx], logic, }; return { ...prevSurvey, blocks }; @@ -369,17 +365,13 @@ export const QuestionsView = ({ }; // Update block logic fallback (block-level property) - const updateBlockLogicFallback = (questionIdx: number, logicFallback: string | undefined) => { - const question = questions[questionIdx]; - if (!question) return; - - const { blockIndex } = findElementLocation(localSurvey, question.id); - if (blockIndex === -1) return; + const updateBlockLogicFallback = (blockIdx: number, logicFallback: string | undefined) => { + if (blockIdx < 0 || blockIdx >= localSurvey.blocks.length) return; setLocalSurvey((prevSurvey) => { const blocks = [...(prevSurvey.blocks ?? [])]; - blocks[blockIndex] = { - ...blocks[blockIndex], + blocks[blockIdx] = { + ...blocks[blockIdx], logicFallback, }; return { ...prevSurvey, blocks }; @@ -580,6 +572,53 @@ export const QuestionsView = ({ internalQuestionIdMap[updatedQuestion.id] = createId(); }; + const moveElementToBlock = (elementId: string, targetBlockId: string) => { + const updatedSurvey = structuredClone(localSurvey); + + // Find the source block and element + let sourceBlock: TSurveyBlock | undefined; + let elementToMove: TSurveyElement | undefined; + let elementIndexInBlock = -1; + + for (const block of updatedSurvey.blocks) { + const idx = block.elements.findIndex((el) => el.id === elementId); + if (idx !== -1) { + sourceBlock = block; + elementToMove = block.elements[idx]; + elementIndexInBlock = idx; + break; + } + } + + if (!sourceBlock || !elementToMove) { + toast.error(t("environments.surveys.edit.element_not_found")); + return; + } + + // Remove element from source block + sourceBlock.elements.splice(elementIndexInBlock, 1); + + // If source block is now empty, delete it + if (sourceBlock.elements.length === 0) { + const blockIdx = updatedSurvey.blocks.findIndex((b) => b.id === sourceBlock.id); + if (blockIdx !== -1) { + updatedSurvey.blocks.splice(blockIdx, 1); + } + } + + // Add element to target block at the end + const targetBlock = updatedSurvey.blocks.find((b) => b.id === targetBlockId); + if (!targetBlock) { + toast.error(t("environments.surveys.edit.target_block_not_found")); + return; + } + + targetBlock.elements.push(elementToMove); + + setLocalSurvey(updatedSurvey); + setActiveQuestionId(elementId); + }; + const addEndingCard = (index: number) => { const updatedSurvey = structuredClone(localSurvey); const newEndingCard = getDefaultEndingCard(localSurvey.languages, t); @@ -721,27 +760,25 @@ export const QuestionsView = ({ }) ); - const onQuestionCardDragEnd = (event: DragEndEvent) => { + const onBlockCardDragEnd = (event: DragEndEvent) => { const { active, over } = event; - // Find source and destination block indices - const sourceQuestion = questions.find((q) => q.id === active.id); - const destQuestion = questions.find((q) => q.id === over?.id); + if (!over) return; - if (!sourceQuestion || !destQuestion) return; + // Check if we're dragging a block (not a question/element) + const sourceBlockIndex = localSurvey.blocks.findIndex((b) => b.id === active.id); + const destBlockIndex = localSurvey.blocks.findIndex((b) => b.id === over.id); - const { blockIndex: sourceBlockIndex } = findElementLocation(localSurvey, sourceQuestion.id); - const { blockIndex: destBlockIndex } = findElementLocation(localSurvey, destQuestion.id); + if (sourceBlockIndex !== -1 && destBlockIndex !== -1) { + // We're dragging blocks + if (sourceBlockIndex === destBlockIndex) return; // No move needed - if (sourceBlockIndex === -1 || destBlockIndex === -1) return; - if (sourceBlockIndex === destBlockIndex) return; // No move needed + const blocks = [...localSurvey.blocks]; + const [movedBlock] = blocks.splice(sourceBlockIndex, 1); + blocks.splice(destBlockIndex, 0, movedBlock); - // Reorder blocks - const blocks = [...(localSurvey.blocks ?? [])]; - const [movedBlock] = blocks.splice(sourceBlockIndex, 1); - blocks.splice(destBlockIndex, 0, movedBlock); - - setLocalSurvey({ ...localSurvey, blocks }); + setLocalSurvey({ ...localSurvey, blocks }); + } }; const onEndingCardDragEnd = (event: DragEndEvent) => { @@ -777,9 +814,9 @@ export const QuestionsView = ({ )} diff --git a/apps/web/modules/survey/editor/components/rating-question-form.tsx b/apps/web/modules/survey/editor/components/rating-question-form.tsx index ce1161da54..0e221d9aeb 100644 --- a/apps/web/modules/survey/editor/components/rating-question-form.tsx +++ b/apps/web/modules/survey/editor/components/rating-question-form.tsx @@ -3,7 +3,6 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import { HashIcon, PlusIcon, SmileIcon, StarIcon } from "lucide-react"; import { useTranslation } from "react-i18next"; -import { TI18nString } from "@formbricks/types/i18n"; import { TSurveyRatingElement } from "@formbricks/types/surveys/elements"; import { TSurvey } from "@formbricks/types/surveys/types"; import { TUserLocale } from "@formbricks/types/user"; @@ -26,7 +25,6 @@ interface RatingQuestionFormProps { locale: TUserLocale; isStorageConfigured: boolean; isExternalUrlsAllowed?: boolean; - buttonLabel?: TI18nString; } export const RatingQuestionForm = ({ @@ -40,7 +38,6 @@ export const RatingQuestionForm = ({ locale, isStorageConfigured = true, isExternalUrlsAllowed, - buttonLabel, }: RatingQuestionFormProps) => { const { t } = useTranslation(); const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages); @@ -181,27 +178,6 @@ export const RatingQuestionForm = ({
-
- {!question.required && ( -
- -
- )} -
- {question.scale !== "star" && ( { } }); - test("should return error when block not found", () => { + test("should return error when trying to delete the last block", () => { const survey = createMockSurvey([createMockBlock("block-1", "Block 1")]); + const result = deleteBlock(survey, "block-1"); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error.message).toBe("Cannot delete the last block in the survey"); + } + }); + + test("should return error when block not found", () => { + const survey = createMockSurvey([ + createMockBlock("block-1", "Block 1"), + createMockBlock("block-2", "Block 2"), + ]); const result = deleteBlock(survey, "nonexistent"); expect(result.ok).toBe(false); diff --git a/apps/web/modules/survey/editor/lib/blocks.ts b/apps/web/modules/survey/editor/lib/blocks.ts index 48479baf53..53562924a4 100644 --- a/apps/web/modules/survey/editor/lib/blocks.ts +++ b/apps/web/modules/survey/editor/lib/blocks.ts @@ -130,6 +130,11 @@ export const updateBlock = ( * @returns Result with updated survey or Error */ export const deleteBlock = (survey: TSurvey, blockId: string): Result => { + // Prevent deleting the last block + if (survey.blocks?.length === 1) { + return err(new Error("Cannot delete the last block in the survey")); + } + const filteredBlocks = survey.blocks?.filter((b) => b.id !== blockId) || []; if (filteredBlocks.length === survey.blocks?.length) { diff --git a/apps/web/modules/survey/editor/lib/shared-conditions-factory.test.ts b/apps/web/modules/survey/editor/lib/shared-conditions-factory.test.ts index c50e261f56..f08975eb03 100644 --- a/apps/web/modules/survey/editor/lib/shared-conditions-factory.test.ts +++ b/apps/web/modules/survey/editor/lib/shared-conditions-factory.test.ts @@ -1,4 +1,5 @@ import { createId } from "@paralleldrive/cuid2"; +import { TFunction } from "i18next"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { TSurveyQuotaLogic } from "@formbricks/types/quota"; import { @@ -87,7 +88,12 @@ vi.mock("@/modules/survey/editor/lib/utils", () => ({ { value: "notEquals", label: "not equals" }, { value: "isEmpty", label: "is empty" }, ]), - getDefaultOperatorForQuestion: vi.fn().mockReturnValue("equals"), + getDefaultOperatorForElement: vi.fn().mockReturnValue("equals"), + getElementOperatorOptions: vi.fn().mockReturnValue([ + { value: "equals", label: "equals" }, + { value: "notEquals", label: "not equals" }, + { value: "isEmpty", label: "is empty" }, + ]), })); vi.mock("@paralleldrive/cuid2", () => ({ @@ -168,7 +174,7 @@ describe("shared-conditions-factory", () => { const defaultParams: SharedConditionsFactoryParams = { survey: mockSurvey, - t: mockT, + t: mockT as unknown as TFunction, getDefaultOperator: mockGetDefaultOperator, }; @@ -243,15 +249,15 @@ describe("shared-conditions-factory", () => { result.config.getLeftOperandOptions(); const { getConditionValueOptions } = await import("@/modules/survey/editor/lib/utils"); - expect(getConditionValueOptions).toHaveBeenCalledWith(mockSurvey, mockT); + expect(getConditionValueOptions).toHaveBeenCalledWith(mockSurvey, mockT, undefined); }); test("should call getConditionValueOptions with questionIdx", async () => { - const paramsWithQuestionIdx = { + const paramsWithBlockIdx = { ...defaultParams, - questionIdx: 0, + blockIdx: 0, }; - const result = createSharedConditionsFactory(paramsWithQuestionIdx, defaultCallbacks); + const result = createSharedConditionsFactory(paramsWithBlockIdx, defaultCallbacks); result.config.getLeftOperandOptions(); @@ -270,15 +276,15 @@ describe("shared-conditions-factory", () => { result.config.getValueProps(mockCondition); const { getMatchValueProps } = await import("@/modules/survey/editor/lib/utils"); - expect(getMatchValueProps).toHaveBeenCalledWith(mockCondition, mockSurvey, mockT); + expect(getMatchValueProps).toHaveBeenCalledWith(mockCondition, mockSurvey, mockT, undefined); }); test("should call getMatchValueProps with questionIdx", async () => { - const paramsWithQuestionIdx = { + const paramsWithBlockIdx = { ...defaultParams, - questionIdx: 0, + blockIdx: 0, }; - const result = createSharedConditionsFactory(paramsWithQuestionIdx, defaultCallbacks); + const result = createSharedConditionsFactory(paramsWithBlockIdx, defaultCallbacks); const mockCondition: TSingleCondition = { id: "condition1", leftOperand: { value: "question1", type: "question" }, @@ -300,6 +306,30 @@ describe("shared-conditions-factory", () => { expect(mockGetDefaultOperator).toHaveBeenCalled(); }); + test("should get operator options for condition", async () => { + const { getConditionOperatorOptions } = await import("@/modules/survey/editor/lib/utils"); + const mockGetConditionOperatorOptions = vi.mocked(getConditionOperatorOptions); + mockGetConditionOperatorOptions.mockReturnValue([ + { value: "equals", label: "equals" }, + { value: "doesNotEqual", label: "does not equal" }, + ]); + + const result = createSharedConditionsFactory(defaultParams, defaultCallbacks); + const mockCondition: TSingleCondition = { + id: "condition1", + leftOperand: { value: "question1", type: "question" }, + operator: "equals", + }; + + const operators = result.config.getOperatorOptions(mockCondition); + + expect(mockGetConditionOperatorOptions).toHaveBeenCalledWith(mockCondition, mockSurvey, mockT); + expect(operators).toEqual([ + { value: "equals", label: "equals" }, + { value: "doesNotEqual", label: "does not equal" }, + ]); + }); + test("should format left operand value", async () => { const { getFormatLeftOperandValue } = await import("@/modules/survey/editor/lib/utils"); const mockGetFormatLeftOperandValue = vi.mocked(getFormatLeftOperandValue); @@ -360,6 +390,139 @@ describe("shared-conditions-factory", () => { expect(mockConditionsChange).toHaveBeenCalledWith(expect.any(Function)); }); + test("onUpdateCondition should correct invalid operator for element type", async () => { + const { getElementOperatorOptions } = await import("@/modules/survey/editor/lib/utils"); + const mockGetElementOperatorOptions = vi.mocked(getElementOperatorOptions); + + // Mock to return limited operators (e.g., only isEmpty and isNotEmpty) + mockGetElementOperatorOptions.mockReturnValue([ + { value: "isEmpty", label: "is empty" }, + { value: "isNotEmpty", label: "is not empty" }, + ]); + + const result = createSharedConditionsFactory(defaultParams, defaultCallbacks); + const resourceId = "condition1"; + const updates = { + leftOperand: { + value: "question1", + type: "question" as const, + }, + operator: "equals" as TSurveyLogicConditionsOperator, // Invalid operator for this element + }; + + result.callbacks.onUpdateCondition(resourceId, updates); + + expect(mockConditionsChange).toHaveBeenCalledWith(expect.any(Function)); + + // Get the updater function that was called + const updater = mockConditionsChange.mock.calls[0][0] as (c: TConditionGroup) => TConditionGroup; + const mockConditions: TConditionGroup = { + id: "root", + connector: "and", + conditions: [ + { + id: "condition1", + leftOperand: { value: "oldQuestion", type: "question" }, + operator: "equals", + }, + ], + }; + + updater(structuredClone(mockConditions)); + + // Verify the operator was validated + expect(mockGetElementOperatorOptions).toHaveBeenCalled(); + }); + + test("onUpdateCondition should handle update with valid operator", async () => { + const { getElementOperatorOptions } = await import("@/modules/survey/editor/lib/utils"); + const mockGetElementOperatorOptions = vi.mocked(getElementOperatorOptions); + + // Mock to return operators that include the one being set + mockGetElementOperatorOptions.mockReturnValue([ + { value: "equals", label: "equals" }, + { value: "doesNotEqual", label: "does not equal" }, + ]); + + const result = createSharedConditionsFactory(defaultParams, defaultCallbacks); + const resourceId = "condition1"; + const updates = { + leftOperand: { + value: "question1", + type: "question" as const, + }, + operator: "equals" as TSurveyLogicConditionsOperator, // Valid operator + }; + + result.callbacks.onUpdateCondition(resourceId, updates); + + expect(mockConditionsChange).toHaveBeenCalledWith(expect.any(Function)); + }); + + test("onUpdateCondition should handle update without leftOperand", () => { + const result = createSharedConditionsFactory(defaultParams, defaultCallbacks); + const resourceId = "condition1"; + const updates = { + operator: "equals" as TSurveyLogicConditionsOperator, + }; + + result.callbacks.onUpdateCondition(resourceId, updates); + + expect(mockConditionsChange).toHaveBeenCalledWith(expect.any(Function)); + }); + + test("onUpdateCondition should handle update without operator", () => { + const result = createSharedConditionsFactory(defaultParams, defaultCallbacks); + const resourceId = "condition1"; + const updates = { + leftOperand: { + value: "question1", + type: "question" as const, + }, + }; + + result.callbacks.onUpdateCondition(resourceId, updates); + + expect(mockConditionsChange).toHaveBeenCalledWith(expect.any(Function)); + }); + + test("onUpdateCondition should handle non-question leftOperand type", () => { + const result = createSharedConditionsFactory(defaultParams, defaultCallbacks); + const resourceId = "condition1"; + const updates = { + leftOperand: { + value: "variable1", + type: "variable" as const, + }, + operator: "equals" as TSurveyLogicConditionsOperator, + }; + + result.callbacks.onUpdateCondition(resourceId, updates); + + expect(mockConditionsChange).toHaveBeenCalledWith(expect.any(Function)); + }); + + test("onUpdateCondition should handle element not found", async () => { + const { getElementOperatorOptions } = await import("@/modules/survey/editor/lib/utils"); + const mockGetElementOperatorOptions = vi.mocked(getElementOperatorOptions); + + const result = createSharedConditionsFactory(defaultParams, defaultCallbacks); + const resourceId = "condition1"; + const updates = { + leftOperand: { + value: "non-existent-question", + type: "question" as const, + }, + operator: "equals" as TSurveyLogicConditionsOperator, + }; + + result.callbacks.onUpdateCondition(resourceId, updates); + + expect(mockConditionsChange).toHaveBeenCalledWith(expect.any(Function)); + // Should not call getElementOperatorOptions if element not found + expect(mockGetElementOperatorOptions).not.toHaveBeenCalled(); + }); + test("onToggleGroupConnector should toggle group connector", () => { const result = createSharedConditionsFactory(defaultParams, defaultCallbacks); const groupId = "group1"; @@ -367,6 +530,21 @@ describe("shared-conditions-factory", () => { result.callbacks.onToggleGroupConnector(groupId); expect(mockConditionsChange).toHaveBeenCalledWith(expect.any(Function)); + + // Execute the updater function to ensure it runs properly + const updater = mockConditionsChange.mock.calls[0][0] as (c: TConditionGroup) => TConditionGroup; + const mockConditions: TConditionGroup = { + id: "root", + connector: "and", + conditions: [ + { + id: "group1", + connector: "and", + conditions: [], + }, + ], + }; + updater(mockConditions); }); test("onCreateGroup should create group when includeCreateGroup is true", () => { @@ -380,6 +558,21 @@ describe("shared-conditions-factory", () => { result.callbacks.onCreateGroup!(resourceId); expect(mockConditionsChange).toHaveBeenCalledWith(expect.any(Function)); + + // Execute the updater function to ensure it runs properly + const updater = mockConditionsChange.mock.calls[0][0] as (c: TConditionGroup) => TConditionGroup; + const mockConditions: TConditionGroup = { + id: "root", + connector: "and", + conditions: [ + { + id: "condition1", + leftOperand: { value: "question1", type: "question" }, + operator: "equals", + }, + ], + }; + updater(mockConditions); }); }); diff --git a/apps/web/modules/survey/editor/lib/shared-conditions-factory.ts b/apps/web/modules/survey/editor/lib/shared-conditions-factory.ts index 6034a395e7..a2b21946bd 100644 --- a/apps/web/modules/survey/editor/lib/shared-conditions-factory.ts +++ b/apps/web/modules/survey/editor/lib/shared-conditions-factory.ts @@ -19,10 +19,10 @@ import { import { getConditionOperatorOptions, getConditionValueOptions, - getDefaultOperatorForQuestion, + getDefaultOperatorForElement, + getElementOperatorOptions, getFormatLeftOperandValue, getMatchValueProps, - getQuestionOperatorOptions, } from "@/modules/survey/editor/lib/utils"; import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils"; import { @@ -35,7 +35,7 @@ import { export interface SharedConditionsFactoryParams { survey: TSurvey; t: TFunction; - questionIdx?: number; + blockIdx?: number; getDefaultOperator: () => TSurveyLogicConditionsOperator; includeCreateGroup?: boolean; } @@ -53,26 +53,26 @@ export function createSharedConditionsFactory( config: TConditionsEditorConfig; callbacks: TConditionsEditorCallbacks; } { - const { survey, t, questionIdx, getDefaultOperator, includeCreateGroup = false } = params; + const { survey, t, blockIdx, getDefaultOperator, includeCreateGroup = false } = params; const { onConditionsChange } = updateCallbacks; - // Derive questions from blocks - const questions = getElementsFromBlocks(survey.blocks); + // Derive elements from blocks + const elements = getElementsFromBlocks(survey.blocks); - // Handles special update logic for matrix questions, setting appropriate operators and metadata - const handleMatrixQuestionUpdate = (resourceId: string, updates: Partial): boolean => { + // Handles special update logic for matrix elements, setting appropriate operators and metadata + const handleMatrixElementUpdate = (resourceId: string, updates: Partial): boolean => { if (updates.leftOperand && updates.leftOperand.type === "question") { - const [questionId, rowId] = updates.leftOperand.value.split("."); - const questionEntity = questions.find((q) => q.id === questionId); + const [elementId, rowId] = updates.leftOperand.value.split("."); + const element = elements.find((q) => q.id === elementId); - if (questionEntity && questionEntity.type === TSurveyElementTypeEnum.Matrix) { + if (element && element.type === TSurveyElementTypeEnum.Matrix) { if (updates.leftOperand.value.includes(".")) { - // Matrix question with rowId is selected + // Matrix element with rowId is selected onConditionsChange((conditions) => { const conditionsCopy = structuredClone(conditions); updateCondition(conditionsCopy, resourceId, { leftOperand: { - value: questionId, + value: elementId, type: "question", meta: { row: rowId, @@ -91,15 +91,9 @@ export function createSharedConditionsFactory( }; const config: TConditionsEditorConfig = { - getLeftOperandOptions: () => - questionIdx !== undefined - ? getConditionValueOptions(survey, t, questionIdx) - : getConditionValueOptions(survey, t), + getLeftOperandOptions: () => getConditionValueOptions(survey, t, blockIdx), getOperatorOptions: (condition) => getConditionOperatorOptions(condition, survey, t), - getValueProps: (condition) => - questionIdx !== undefined - ? getMatchValueProps(condition, survey, t, questionIdx) - : getMatchValueProps(condition, survey, t), + getValueProps: (condition) => getMatchValueProps(condition, survey, t, blockIdx), getDefaultOperator, formatLeftOperandValue: (condition) => getFormatLeftOperandValue(condition, survey), }; @@ -107,12 +101,9 @@ export function createSharedConditionsFactory( const callbacks: TConditionsEditorCallbacks = { // Creates and adds a new empty condition below the specified condition onAddConditionBelow: (resourceId: string) => { - // When adding a condition in the context of a specific question, default to that question - const defaultLeftOperandValue = questionIdx !== undefined ? questions[questionIdx].id : questions[0].id; - const defaultOperator = - questionIdx !== undefined - ? getDefaultOperatorForQuestion(questions[questionIdx], t) - : getDefaultOperatorForQuestion(questions[0], t); + // When adding a condition in the context of a specific block, default to the first element + const defaultLeftOperandValue = elements.length > 0 ? elements[0].id : ""; + const defaultOperator = elements.length > 0 ? getDefaultOperatorForElement(elements[0], t) : "equals"; const newCondition: TSingleCondition = { id: createId(), leftOperand: { value: defaultLeftOperandValue, type: "question" }, @@ -144,20 +135,20 @@ export function createSharedConditionsFactory( }); }, - // Updates a condition with new values, handling matrix questions specially + // Updates a condition with new values, handling matrix elements specially onUpdateCondition: (resourceId: string, updates: Partial) => { - // Try matrix question handling first - if (handleMatrixQuestionUpdate(resourceId, updates)) { + // Try matrix element handling first + if (handleMatrixElementUpdate(resourceId, updates)) { return; } - // Check if the operator is correct for the question + // Check if the operator is correct for the element if (updates.leftOperand?.type === "question" && updates.operator) { - const questionId = updates.leftOperand.value.split(".")[0]; - const question = questions.find((q) => q.id === questionId); + const elementId = updates.leftOperand.value.split(".")[0]; + const element = elements.find((q) => q.id === elementId); - if (question) { - const operatorOptions = getQuestionOperatorOptions(question, t); + if (element) { + const operatorOptions = getElementOperatorOptions(element, t); const isValidOperator = operatorOptions.some((o) => o.value === updates.operator); if (!isValidOperator) { diff --git a/apps/web/modules/survey/editor/lib/utils.tsx b/apps/web/modules/survey/editor/lib/utils.tsx index f9f3e89467..bd4dd3feea 100644 --- a/apps/web/modules/survey/editor/lib/utils.tsx +++ b/apps/web/modules/survey/editor/lib/utils.tsx @@ -26,7 +26,7 @@ import { isConditionGroup } from "@/lib/surveyLogic/utils"; import { recallToHeadline } from "@/lib/utils/recall"; import { findElementLocation } from "@/modules/survey/editor/lib/blocks"; import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils"; -import { getQuestionTypes } from "@/modules/survey/lib/questions"; +import { getQuestionTypes, getTSurveyQuestionTypeEnumName } from "@/modules/survey/lib/questions"; import { TComboboxGroupedOption, TComboboxOption } from "@/modules/ui/components/input-combo-box"; import { TLogicRuleOption, getLogicRules } from "./logic-rule-engine"; @@ -106,74 +106,92 @@ const getQuestionIconMapping = (t: TFunction) => {} ); +const getElementHeadline = ( + localSurvey: TSurvey, + element: TSurveyElement, + languageCode: string, + t: TFunction +): string => { + const headlineData = recallToHeadline(element.headline, localSurvey, false, languageCode); + const headlineText = headlineData[languageCode]; + if (headlineText) { + const textContent = getTextContent(headlineText); + if (textContent.length > 0) { + return textContent; + } + } + return getTSurveyQuestionTypeEnumName(element.type, t) ?? ""; +}; + export const getConditionValueOptions = ( localSurvey: TSurvey, t: TFunction, - currQuestionIdx?: number // Optional in case of quotas + blockIdx?: number // Optional - if provided, includes elements from this block and all previous blocks ): TComboboxGroupedOption[] => { const hiddenFields = localSurvey.hiddenFields?.fieldIds ?? []; const variables = localSurvey.variables ?? []; - // Derive questions from blocks - const questions = getElementsFromBlocks(localSurvey.blocks); + + // If blockIdx is provided, get elements from current block and all previous blocks + // Otherwise, get all elements from all blocks + const allElements = + blockIdx === undefined + ? getElementsFromBlocks(localSurvey.blocks) + : localSurvey.blocks.slice(0, blockIdx + 1).flatMap((block) => block.elements); const groupedOptions: TComboboxGroupedOption[] = []; - const questionOptions: TComboboxOption[] = []; + const elementOptions: TComboboxOption[] = []; - questions - .filter((_, idx) => (typeof currQuestionIdx === "undefined" ? true : idx <= currQuestionIdx)) - .forEach((question) => { - if (question.type === TSurveyElementTypeEnum.Matrix) { - // Rows submenu - const processedHeadline = recallToHeadline(question.headline, localSurvey, false, "default"); - const questionHeadline = getTextContent(processedHeadline.default ?? ""); - const rows = question.rows.map((row, rowIdx) => { - const processedLabel = recallToHeadline(row.label, localSurvey, false, "default"); - return { - icon: getQuestionIconMapping(t)[question.type], - label: `${getTextContent(processedLabel.default ?? "")} (${questionHeadline})`, - value: `${question.id}.${rowIdx}`, + allElements.forEach((element) => { + if (element.type === TSurveyElementTypeEnum.Matrix) { + const elementHeadline = getElementHeadline(localSurvey, element, "default", t); + + // Rows submenu + const rows = element.rows.map((row, rowIdx) => { + const processedLabel = recallToHeadline(row.label, localSurvey, false, "default"); + return { + icon: getQuestionIconMapping(t)[element.type], + label: `${getTextContent(processedLabel.default ?? "")} (${elementHeadline})`, + value: `${element.id}.${rowIdx}`, + meta: { + type: "question", + rowIdx: rowIdx.toString(), + }, + }; + }); + + elementOptions.push({ + icon: getQuestionIconMapping(t)[element.type], + label: elementHeadline, + value: element.id, + meta: { + type: "question", + }, + children: [ + { + label: t("environments.surveys.edit.matrix_rows", "Rows"), + value: `${element.id}-rows`, + children: rows, + }, + { + label: t("environments.surveys.edit.matrix_all_fields", "All fields"), + value: element.id, meta: { type: "question", - rowIdx: rowIdx.toString(), }, - }; - }); - - questionOptions.push({ - icon: getQuestionIconMapping(t)[question.type], - label: questionHeadline, - value: question.id, - meta: { - type: "question", }, - children: [ - { - label: t("environments.surveys.edit.matrix_rows", "Rows"), - value: `${question.id}-rows`, - children: rows, - }, - { - label: t("environments.surveys.edit.matrix_all_fields", "All fields"), - value: question.id, - meta: { - type: "question", - }, - }, - ], - }); - } else { - questionOptions.push({ - icon: getQuestionIconMapping(t)[question.type], - label: getTextContent( - recallToHeadline(question.headline, localSurvey, false, "default").default ?? "" - ), - value: question.id, - meta: { - type: "question", - }, - }); - } - }); + ], + }); + } else { + elementOptions.push({ + icon: getQuestionIconMapping(t)[element.type], + label: getElementHeadline(localSurvey, element, "default", t), + value: element.id, + meta: { + type: "question", + }, + }); + } + }); const variableOptions = variables.map((variable) => { return { @@ -197,11 +215,11 @@ export const getConditionValueOptions = ( }; }); - if (questionOptions.length > 0) { + if (elementOptions.length > 0) { groupedOptions.push({ label: t("common.questions"), value: "questions", - options: questionOptions, + options: elementOptions, }); } @@ -237,43 +255,43 @@ export const replaceEndingCardHeadlineRecall = (survey: TSurvey, language: strin export const getActionObjectiveOptions = (t: TFunction): TComboboxOption[] => [ { label: t("environments.surveys.edit.calculate"), value: "calculate" }, { label: t("environments.surveys.edit.require_answer"), value: "requireAnswer" }, - { label: t("environments.surveys.edit.jump_to_question"), value: "jumpToBlock" }, + { label: t("environments.surveys.edit.jump_to_block"), value: "jumpToBlock" }, ]; export const hasJumpToBlockAction = (actions: TSurveyBlockLogicAction[]): boolean => { return actions.some((action) => action.objective === "jumpToBlock"); }; -export const getQuestionOperatorOptions = ( - question: TSurveyElement, +export const getElementOperatorOptions = ( + element: TSurveyElement, t: TFunction, condition?: TSingleCondition ): TComboboxOption[] => { let options: TLogicRuleOption; - if (question.type === "openText") { - const inputType = question.inputType === "number" ? "number" : "text"; + if (element.type === "openText") { + const inputType = element.inputType === "number" ? "number" : "text"; options = getLogicRules(t).question[`openText.${inputType}`].options; - } else if (question.type === TSurveyElementTypeEnum.Matrix && condition) { + } else if (element.type === TSurveyElementTypeEnum.Matrix && condition) { const isMatrixRow = condition.leftOperand.type === "question" && condition.leftOperand?.meta?.row !== undefined; options = getLogicRules(t).question[`matrix${isMatrixRow ? ".row" : ""}`].options; } else { - options = getLogicRules(t).question[question.type].options; + options = getLogicRules(t).question[element.type].options; } - if (question.required) { + if (element.required) { options = options.filter((option) => option.value !== "isSkipped") as TLogicRuleOption; } return options; }; -export const getDefaultOperatorForQuestion = ( - question: TSurveyElement, +export const getDefaultOperatorForElement = ( + element: TSurveyElement, t: TFunction ): TSurveyLogicConditionsOperator => { - const options = getQuestionOperatorOptions(question, t); + const options = getElementOperatorOptions(element, t); return options[0].value.toString() as TSurveyLogicConditionsOperator; }; @@ -305,8 +323,8 @@ export const getConditionOperatorOptions = ( return getLogicRules(t).hiddenField.options; } else if (condition.leftOperand.type === "question") { // Derive questions from blocks - const questions = getElementsFromBlocks(localSurvey.blocks); - const question = questions.find((question) => { + const elements = getElementsFromBlocks(localSurvey.blocks); + const element = elements.find((question) => { let leftOperandQuestionId = condition.leftOperand.value; if (question.type === TSurveyElementTypeEnum.Matrix) { leftOperandQuestionId = condition.leftOperand.value.split(".")[0]; @@ -314,9 +332,9 @@ export const getConditionOperatorOptions = ( return question.id === leftOperandQuestionId; }); - if (!question) return []; + if (!element) return []; - return getQuestionOperatorOptions(question, t, condition); + return getElementOperatorOptions(element, t, condition); } return []; }; @@ -325,7 +343,7 @@ export const getMatchValueProps = ( condition: TSingleCondition, localSurvey: TSurvey, t: TFunction, - questionIdx?: number + blockIdx?: number // Optional - if provided, includes elements from this block and all previous blocks ): { show?: boolean; showInput?: boolean; @@ -350,19 +368,23 @@ export const getMatchValueProps = ( return { show: false, options: [] }; } - // Derive questions from blocks - const allQuestions = getElementsFromBlocks(localSurvey.blocks); - let questions = allQuestions.filter((_, idx) => - typeof questionIdx === "undefined" ? true : idx <= questionIdx - ); + // If blockIdx is provided, get elements from current block and all previous blocks + // Otherwise, get all elements from all blocks + let elements = + blockIdx === undefined + ? getElementsFromBlocks(localSurvey.blocks) + : localSurvey.blocks + .slice(0, blockIdx + 1) // Include blocks from 0 to blockIdx (inclusive) + .flatMap((block) => block.elements); + let variables = localSurvey.variables ?? []; let hiddenFields = localSurvey.hiddenFields?.fieldIds ?? []; - const selectedQuestion = questions.find((question) => question.id === condition.leftOperand.value); + const selectedElement = elements.find((element) => element.id === condition.leftOperand.value); const selectedVariable = variables.find((variable) => variable.id === condition.leftOperand.value); if (condition.leftOperand.type === "question") { - questions = questions.filter((question) => question.id !== condition.leftOperand.value); + elements = elements.filter((element) => element.id !== condition.leftOperand.value); } else if (condition.leftOperand.type === "variable") { variables = variables.filter((variable) => variable.id !== condition.leftOperand.value); } else if (condition.leftOperand.type === "hiddenField") { @@ -370,16 +392,16 @@ export const getMatchValueProps = ( } if (condition.leftOperand.type === "question") { - if (selectedQuestion?.type === TSurveyElementTypeEnum.OpenText) { - const allowedQuestionTypes = [TSurveyElementTypeEnum.OpenText]; + if (selectedElement?.type === TSurveyElementTypeEnum.OpenText) { + const allowedElementTypes = [TSurveyElementTypeEnum.OpenText]; - if (selectedQuestion.inputType === "number") { - allowedQuestionTypes.push(TSurveyElementTypeEnum.Rating, TSurveyElementTypeEnum.NPS); + if (selectedElement.inputType === "number") { + allowedElementTypes.push(TSurveyElementTypeEnum.Rating, TSurveyElementTypeEnum.NPS); } if (["equals", "doesNotEqual"].includes(condition.operator)) { - if (selectedQuestion.inputType !== "number") { - allowedQuestionTypes.push( + if (selectedElement.inputType !== "number") { + allowedElementTypes.push( TSurveyElementTypeEnum.Date, TSurveyElementTypeEnum.MultipleChoiceSingle, TSurveyElementTypeEnum.MultipleChoiceMulti @@ -387,15 +409,15 @@ export const getMatchValueProps = ( } } - const allowedQuestions = questions.filter((question) => allowedQuestionTypes.includes(question.type)); + const allowedElements = elements.filter((element) => allowedElementTypes.includes(element.type)); - const questionOptions = allowedQuestions.map((question) => { + const elementOptions = allowedElements.map((element) => { return { - icon: getQuestionIconMapping(t)[question.type], + icon: getQuestionIconMapping(t)[element.type], label: getTextContent( - recallToHeadline(question.headline, localSurvey, false, "default").default ?? "" + recallToHeadline(element.headline, localSurvey, false, "default").default ?? "" ), - value: question.id, + value: element.id, meta: { type: "question", }, @@ -404,7 +426,7 @@ export const getMatchValueProps = ( const variableOptions = variables .filter((variable) => - selectedQuestion.inputType !== "number" ? variable.type === "text" : variable.type === "number" + selectedElement.inputType === "number" ? variable.type === "number" : variable.type === "text" ) .map((variable) => { return { @@ -430,11 +452,11 @@ export const getMatchValueProps = ( const groupedOptions: TComboboxGroupedOption[] = []; - if (questionOptions.length > 0) { + if (elementOptions.length > 0) { groupedOptions.push({ label: t("common.questions"), value: "questions", - options: questionOptions, + options: elementOptions, }); } @@ -456,12 +478,12 @@ export const getMatchValueProps = ( return { show: true, showInput: true, - inputType: selectedQuestion.inputType === "number" ? "number" : "text", + inputType: selectedElement.inputType === "number" ? "number" : "text", options: groupedOptions, }; } else if ( - selectedQuestion?.type === TSurveyElementTypeEnum.MultipleChoiceSingle || - selectedQuestion?.type === TSurveyElementTypeEnum.MultipleChoiceMulti + selectedElement?.type === TSurveyElementTypeEnum.MultipleChoiceSingle || + selectedElement?.type === TSurveyElementTypeEnum.MultipleChoiceMulti ) { const operatorsToFilterNone = [ "includesOneOf", @@ -470,10 +492,10 @@ export const getMatchValueProps = ( "doesNotIncludeAllOf", ]; const shouldFilterNone = - selectedQuestion.type === TSurveyElementTypeEnum.MultipleChoiceMulti && + selectedElement.type === TSurveyElementTypeEnum.MultipleChoiceMulti && operatorsToFilterNone.includes(condition.operator); - const choices = selectedQuestion.choices + const choices = selectedElement.choices .filter((choice) => !shouldFilterNone || choice.id !== "none") .map((choice) => { return { @@ -490,8 +512,8 @@ export const getMatchValueProps = ( showInput: false, options: [{ label: t("common.choices"), value: "choices", options: choices }], }; - } else if (selectedQuestion?.type === TSurveyElementTypeEnum.PictureSelection) { - const choices = selectedQuestion.choices.map((choice, idx) => { + } else if (selectedElement?.type === TSurveyElementTypeEnum.PictureSelection) { + const choices = selectedElement.choices.map((choice, idx) => { return { imgSrc: choice.imageUrl, label: `${t("common.picture")} ${idx + 1}`, @@ -507,8 +529,8 @@ export const getMatchValueProps = ( showInput: false, options: [{ label: t("common.choices"), value: "choices", options: choices }], }; - } else if (selectedQuestion?.type === TSurveyElementTypeEnum.Rating) { - const choices = Array.from({ length: selectedQuestion.range }, (_, idx) => { + } else if (selectedElement?.type === TSurveyElementTypeEnum.Rating) { + const choices = Array.from({ length: selectedElement.range }, (_, idx) => { return { label: `${idx + 1}`, value: idx + 1, @@ -554,7 +576,7 @@ export const getMatchValueProps = ( showInput: false, options: groupedOptions, }; - } else if (selectedQuestion?.type === TSurveyElementTypeEnum.NPS) { + } else if (selectedElement?.type === TSurveyElementTypeEnum.NPS) { const choices = Array.from({ length: 11 }, (_, idx) => { return { label: `${idx}`, @@ -601,18 +623,18 @@ export const getMatchValueProps = ( showInput: false, options: groupedOptions, }; - } else if (selectedQuestion?.type === TSurveyElementTypeEnum.Date) { - const openTextQuestions = questions.filter((question) => - [TSurveyElementTypeEnum.OpenText, TSurveyElementTypeEnum.Date].includes(question.type) + } else if (selectedElement?.type === TSurveyElementTypeEnum.Date) { + const openTextElements = elements.filter((element) => + [TSurveyElementTypeEnum.OpenText, TSurveyElementTypeEnum.Date].includes(element.type) ); - const questionOptions = openTextQuestions.map((question) => { + const elementOptions = openTextElements.map((element) => { return { - icon: getQuestionIconMapping(t)[question.type], + icon: getQuestionIconMapping(t)[element.type], label: getTextContent( - recallToHeadline(question.headline, localSurvey, false, "default").default ?? "" + recallToHeadline(element.headline, localSurvey, false, "default").default ?? "" ), - value: question.id, + value: element.id, meta: { type: "question", }, @@ -645,11 +667,11 @@ export const getMatchValueProps = ( const groupedOptions: TComboboxGroupedOption[] = []; - if (questionOptions.length > 0) { + if (elementOptions.length > 0) { groupedOptions.push({ label: t("common.questions"), value: "questions", - options: questionOptions, + options: elementOptions, }); } @@ -675,8 +697,8 @@ export const getMatchValueProps = ( inputType: "date", options: groupedOptions, }; - } else if (selectedQuestion?.type === TSurveyElementTypeEnum.Matrix) { - const choices = selectedQuestion.columns.map((column, colIdx) => { + } else if (selectedElement?.type === TSurveyElementTypeEnum.Matrix) { + const choices = selectedElement.columns.map((column, colIdx) => { return { label: getLocalizedValue(column.label, "default"), value: colIdx.toString(), @@ -694,22 +716,22 @@ export const getMatchValueProps = ( } } else if (condition.leftOperand.type === "variable") { if (selectedVariable?.type === "text") { - const allowedQuestionTypes = [ + const allowedElementTypes = [ TSurveyElementTypeEnum.OpenText, TSurveyElementTypeEnum.MultipleChoiceSingle, ]; if (["equals", "doesNotEqual"].includes(condition.operator)) { - allowedQuestionTypes.push(TSurveyElementTypeEnum.MultipleChoiceMulti, TSurveyElementTypeEnum.Date); + allowedElementTypes.push(TSurveyElementTypeEnum.MultipleChoiceMulti, TSurveyElementTypeEnum.Date); } - const allowedQuestions = questions.filter((question) => allowedQuestionTypes.includes(question.type)); + const allowedElements = elements.filter((element) => allowedElementTypes.includes(element.type)); - const questionOptions = allowedQuestions.map((question) => { + const elementOptions = allowedElements.map((element) => { return { - icon: getQuestionIconMapping(t)[question.type], - label: getLocalizedValue(question.headline, "default"), - value: question.id, + icon: getQuestionIconMapping(t)[element.type], + label: getElementHeadline(localSurvey, element, "default", t), + value: element.id, meta: { type: "question", }, @@ -742,11 +764,11 @@ export const getMatchValueProps = ( const groupedOptions: TComboboxGroupedOption[] = []; - if (questionOptions.length > 0) { + if (elementOptions.length > 0) { groupedOptions.push({ label: t("common.questions"), value: "questions", - options: questionOptions, + options: elementOptions, }); } @@ -773,17 +795,17 @@ export const getMatchValueProps = ( options: groupedOptions, }; } else if (selectedVariable?.type === "number") { - const allowedQuestions = questions.filter( - (question) => - [TSurveyElementTypeEnum.Rating, TSurveyElementTypeEnum.NPS].includes(question.type) || - (question.type === TSurveyElementTypeEnum.OpenText && question.inputType === "number") + const allowedElements = elements.filter( + (element) => + [TSurveyElementTypeEnum.Rating, TSurveyElementTypeEnum.NPS].includes(element.type) || + (element.type === TSurveyElementTypeEnum.OpenText && element.inputType === "number") ); - const questionOptions = allowedQuestions.map((question) => { + const elementOptions = allowedElements.map((element) => { return { - icon: getQuestionIconMapping(t)[question.type], - label: getLocalizedValue(question.headline, "default"), - value: question.id, + icon: getQuestionIconMapping(t)[element.type], + label: getElementHeadline(localSurvey, element, "default", t), + value: element.id, meta: { type: "question", }, @@ -816,11 +838,11 @@ export const getMatchValueProps = ( const groupedOptions: TComboboxGroupedOption[] = []; - if (questionOptions.length > 0) { + if (elementOptions.length > 0) { groupedOptions.push({ label: t("common.questions"), value: "questions", - options: questionOptions, + options: elementOptions, }); } @@ -848,22 +870,22 @@ export const getMatchValueProps = ( }; } } else if (condition.leftOperand.type === "hiddenField") { - const allowedQuestionTypes = [ + const allowedElementTypes = [ TSurveyElementTypeEnum.OpenText, TSurveyElementTypeEnum.MultipleChoiceSingle, ]; if (["equals", "doesNotEqual"].includes(condition.operator)) { - allowedQuestionTypes.push(TSurveyElementTypeEnum.MultipleChoiceMulti, TSurveyElementTypeEnum.Date); + allowedElementTypes.push(TSurveyElementTypeEnum.MultipleChoiceMulti, TSurveyElementTypeEnum.Date); } - const allowedQuestions = questions.filter((question) => allowedQuestionTypes.includes(question.type)); + const allowedElements = elements.filter((element) => allowedElementTypes.includes(element.type)); - const questionOptions = allowedQuestions.map((question) => { + const elementOptions = allowedElements.map((element) => { return { - icon: getQuestionIconMapping(t)[question.type], - label: getLocalizedValue(question.headline, "default"), - value: question.id, + icon: getQuestionIconMapping(t)[element.type], + label: getElementHeadline(localSurvey, element, "default", t), + value: element.id, meta: { type: "question", }, @@ -896,11 +918,11 @@ export const getMatchValueProps = ( const groupedOptions: TComboboxGroupedOption[] = []; - if (questionOptions.length > 0) { + if (elementOptions.length > 0) { groupedOptions.push({ label: t("common.questions"), value: "questions", - options: questionOptions, + options: elementOptions, }); } @@ -934,50 +956,45 @@ export const getMatchValueProps = ( export const getActionTargetOptions = ( action: TSurveyBlockLogicAction, localSurvey: TSurvey, - currQuestionIdx: number, + blockIdx: number, t: TFunction ): TComboboxOption[] => { - // Derive questions from blocks - const allQuestions = localSurvey.blocks?.flatMap((b) => b.elements) ?? []; - let questions = allQuestions.filter((_, idx) => idx > currQuestionIdx); + // Derive elements from blocks + const allElements = localSurvey.blocks?.flatMap((b) => b.elements) ?? []; + // Calculate which elements come after the current block + let elementsUpToAndIncludingCurrentBlock = 0; + for (let i = 0; i <= blockIdx; i++) { + elementsUpToAndIncludingCurrentBlock += localSurvey.blocks[i].elements.length; + } + + // For requireAnswer, show elements after the current block (not including current block) if (action.objective === "requireAnswer") { - questions = questions.filter((question) => !question.required); - // Return question IDs (elements) for requireAnswer - return questions.map((question) => { - const processedHeadline = recallToHeadline(question.headline, localSurvey, false, "default"); + const elementsAfterCurrentBlock = allElements.filter( + (_, idx) => idx >= elementsUpToAndIncludingCurrentBlock + ); + const nonRequiredElements = elementsAfterCurrentBlock.filter((element) => !element.required); + + // Return element IDs for requireAnswer + return nonRequiredElements.map((element) => { return { - icon: getQuestionIconMapping(t)[question.type], - label: getTextContent(processedHeadline.default ?? ""), - value: question.id, // Element ID + icon: getQuestionIconMapping(t)[element.type], + label: getElementHeadline(localSurvey, element, "default", t), + value: element.id, }; }); } // For jumpToBlock, we need block IDs - // Track which blocks we've already added to avoid duplicates when a block has multiple elements const blocks = localSurvey.blocks ?? []; - const addedBlockIds = new Set(); - const questionOptions: TComboboxOption[] = []; + const blockOptions: TComboboxOption[] = []; - for (const question of questions) { - // Find which block this question belongs to - const block = blocks.find((b) => b.elements.some((e) => e.id === question.id)); + // Add blocks after the current block + for (let i = blockIdx + 1; i < blocks.length; i++) { + const block = blocks[i]; - if (!block) continue; - - // Skip if we've already added this block - if (addedBlockIds.has(block.id)) continue; - - // Mark this block as added - addedBlockIds.add(block.id); - - // Use the first element's headline as the block label - const firstElement = block.elements[0]; - const processedHeadline = recallToHeadline(firstElement.headline, localSurvey, false, "default"); - questionOptions.push({ - icon: getQuestionIconMapping(t)[firstElement.type], - label: getTextContent(processedHeadline.default ?? ""), + blockOptions.push({ + label: block.name, value: block.id, }); } @@ -1004,7 +1021,7 @@ export const getActionTargetOptions = ( } }); - return [...questionOptions, ...endingCardOptions]; + return [...blockOptions, ...endingCardOptions]; }; export const getActionVariableOptions = (localSurvey: TSurvey): TComboboxOption[] => { @@ -1067,13 +1084,15 @@ export const getActionOperatorOptions = ( export const getActionValueOptions = ( variableId: string, localSurvey: TSurvey, - questionIdx: number, + blockIdx: number, t: TFunction ): TComboboxGroupedOption[] => { - const questions = getElementsFromBlocks(localSurvey.blocks); + // Get elements from current block and all previous blocks + const allElements = localSurvey.blocks + .slice(0, blockIdx + 1) // Include blocks from 0 to blockIdx (inclusive) + .flatMap((block) => block.elements); const hiddenFields = localSurvey.hiddenFields?.fieldIds ?? []; let variables = localSurvey.variables ?? []; - const filteredQuestions = questions.filter((_, idx) => idx <= questionIdx); const hiddenFieldsOptions = hiddenFields.map((field) => { return { @@ -1093,21 +1112,21 @@ export const getActionValueOptions = ( if (!selectedVariable) return []; if (selectedVariable.type === "text") { - const allowedQuestions = filteredQuestions.filter((question) => + const allowedElements = allElements.filter((element) => [ TSurveyElementTypeEnum.OpenText, TSurveyElementTypeEnum.MultipleChoiceSingle, TSurveyElementTypeEnum.Rating, TSurveyElementTypeEnum.NPS, TSurveyElementTypeEnum.Date, - ].includes(question.type) + ].includes(element.type) ); - const questionOptions = allowedQuestions.map((question) => { + const elementOptions = allowedElements.map((element) => { return { - icon: getQuestionIconMapping(t)[question.type], - label: getTextContent(getLocalizedValue(question.headline, "default")), - value: question.id, + icon: getQuestionIconMapping(t)[element.type], + label: getElementHeadline(localSurvey, element, "default", t), + value: element.id, meta: { type: "question", }, @@ -1129,11 +1148,11 @@ export const getActionValueOptions = ( const groupedOptions: TComboboxGroupedOption[] = []; - if (questionOptions.length > 0) { + if (elementOptions.length > 0) { groupedOptions.push({ label: t("common.questions"), value: "questions", - options: questionOptions, + options: elementOptions, }); } @@ -1155,17 +1174,17 @@ export const getActionValueOptions = ( return groupedOptions; } else if (selectedVariable.type === "number") { - const allowedQuestions = filteredQuestions.filter( - (question) => - [TSurveyElementTypeEnum.Rating, TSurveyElementTypeEnum.NPS].includes(question.type) || - (question.type === TSurveyElementTypeEnum.OpenText && question.inputType === "number") + const allowedElements = allElements.filter( + (element) => + [TSurveyElementTypeEnum.Rating, TSurveyElementTypeEnum.NPS].includes(element.type) || + (element.type === TSurveyElementTypeEnum.OpenText && element.inputType === "number") ); - const questionOptions = allowedQuestions.map((question) => { + const elementOptions = allowedElements.map((element) => { return { - icon: getQuestionIconMapping(t)[question.type], - label: getTextContent(getLocalizedValue(question.headline, "default")), - value: question.id, + icon: getQuestionIconMapping(t)[element.type], + label: getTextContent(getLocalizedValue(element.headline, "default")), + value: element.id, meta: { type: "question", }, @@ -1187,11 +1206,11 @@ export const getActionValueOptions = ( const groupedOptions: TComboboxGroupedOption[] = []; - if (questionOptions.length > 0) { + if (elementOptions.length > 0) { groupedOptions.push({ label: t("common.questions"), value: "questions", - options: questionOptions, + options: elementOptions, }); } diff --git a/apps/web/modules/survey/lib/questions.tsx b/apps/web/modules/survey/lib/questions.tsx index d2d52c9c68..91fbe080c6 100644 --- a/apps/web/modules/survey/lib/questions.tsx +++ b/apps/web/modules/survey/lib/questions.tsx @@ -171,6 +171,7 @@ export const getQuestionTypes = (t: TFunction): TQuestion[] => [ subheader: createI18nString("", []), ctaButtonLabel: createI18nString(t("templates.book_interview"), []), buttonUrl: "", + buttonExternal: true, }, }, { diff --git a/apps/web/modules/ui/components/tooltip/index.tsx b/apps/web/modules/ui/components/tooltip/index.tsx index dc701ea45e..5f0a80d261 100644 --- a/apps/web/modules/ui/components/tooltip/index.tsx +++ b/apps/web/modules/ui/components/tooltip/index.tsx @@ -32,7 +32,7 @@ const TooltipContent: React.ComponentType )); TooltipContent.displayName = TooltipPrimitive.Content.displayName; -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; +export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; interface TooltipRendererProps { tooltipContent: ReactNode; @@ -40,12 +40,13 @@ interface TooltipRendererProps { className?: string; triggerClass?: string; shouldRender?: boolean; + delayDuration?: number; } export const TooltipRenderer = (props: TooltipRendererProps) => { - const { children, shouldRender = true, tooltipContent, className, triggerClass } = props; + const { children, shouldRender = true, tooltipContent, className, triggerClass, delayDuration = 0 } = props; if (shouldRender) { return ( - + {children} diff --git a/packages/surveys/src/components/general/block-conditional.tsx b/packages/surveys/src/components/general/block-conditional.tsx index f6dc6955fc..3885f29010 100644 --- a/packages/surveys/src/components/general/block-conditional.tsx +++ b/packages/surveys/src/components/general/block-conditional.tsx @@ -27,7 +27,6 @@ interface BlockConditionalProps { setTtc: (ttc: TResponseTtc) => void; surveyId: string; autoFocusEnabled: boolean; - currentBlockId: string; isBackButtonHidden: boolean; onOpenExternalURL?: (url: string) => void | Promise; dir?: "ltr" | "rtl" | "auto"; @@ -208,8 +207,6 @@ export function BlockConditional({ onChange={(responseData) => handleElementChange(element.id, responseData)} onBack={() => {}} onFileUpload={onFileUpload} - isFirstElement={false} - isLastElement={false} languageCode={languageCode} prefilledElementValue={prefilledResponseData?.[element.id]} skipPrefilled={skipPrefilled} diff --git a/packages/surveys/src/components/general/element-conditional.tsx b/packages/surveys/src/components/general/element-conditional.tsx index 030f0f5262..a7e1c30d9e 100644 --- a/packages/surveys/src/components/general/element-conditional.tsx +++ b/packages/surveys/src/components/general/element-conditional.tsx @@ -30,8 +30,6 @@ interface ElementConditionalProps { onChange: (responseData: TResponseData) => void; onBack: () => void; onFileUpload: (file: TJsFileUploadParams["file"], config?: TUploadFileConfig) => Promise; - isFirstElement: boolean; - isLastElement: boolean; languageCode: string; prefilledElementValue?: TResponseDataValue; skipPrefilled?: boolean; diff --git a/packages/surveys/src/components/general/survey.tsx b/packages/surveys/src/components/general/survey.tsx index ca5892719a..a8911fadda 100644 --- a/packages/surveys/src/components/general/survey.tsx +++ b/packages/surveys/src/components/general/survey.tsx @@ -777,7 +777,6 @@ export function Survey({ isLastBlock={block.id === localSurvey.blocks[localSurvey.blocks.length - 1].id} languageCode={selectedLanguage} autoFocusEnabled={autoFocusEnabled} - currentBlockId={blockId} isBackButtonHidden={localSurvey.isBackButtonHidden} onOpenExternalURL={onOpenExternalURL} dir={dir} diff --git a/packages/surveys/src/components/icons/link-icon.tsx b/packages/surveys/src/components/icons/link-icon.tsx index 8f1781a5dd..76e78f2887 100644 --- a/packages/surveys/src/components/icons/link-icon.tsx +++ b/packages/surveys/src/components/icons/link-icon.tsx @@ -11,7 +11,7 @@ export const LinkIcon = ({ className }: LinkIconProps) => { viewBox="0 0 24 24" fill="none" stroke="currentColor" - stroke-width="2" + strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> diff --git a/packages/surveys/src/components/questions/address-question.tsx b/packages/surveys/src/components/questions/address-question.tsx index 8e8c1b069d..31b046c1a3 100644 --- a/packages/surveys/src/components/questions/address-question.tsx +++ b/packages/surveys/src/components/questions/address-question.tsx @@ -36,44 +36,58 @@ export function AddressQuestion({ const [startTime, setStartTime] = useState(performance.now()); const isMediaAvailable = question.imageUrl || question.videoUrl; const formRef = useRef(null); + useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId); + const safeValue = useMemo(() => { return Array.isArray(value) ? value : ["", "", "", "", "", ""]; }, [value]); + const isCurrent = question.id === currentQuestionId; - const fields = [ - { - id: "addressLine1", - ...question.addressLine1, - label: question.addressLine1.placeholder[languageCode], - }, - { - id: "addressLine2", - ...question.addressLine2, - label: question.addressLine2.placeholder[languageCode], - }, - { - id: "city", - ...question.city, - label: question.city.placeholder[languageCode], - }, - { - id: "state", - ...question.state, - label: question.state.placeholder[languageCode], - }, - { - id: "zip", - ...question.zip, - label: question.zip.placeholder[languageCode], - }, - { - id: "country", - ...question.country, - label: question.country.placeholder[languageCode], - }, - ]; + const fields = useMemo( + () => [ + { + id: "addressLine1", + ...question.addressLine1, + label: question.addressLine1.placeholder[languageCode], + }, + { + id: "addressLine2", + ...question.addressLine2, + label: question.addressLine2.placeholder[languageCode], + }, + { + id: "city", + ...question.city, + label: question.city.placeholder[languageCode], + }, + { + id: "state", + ...question.state, + label: question.state.placeholder[languageCode], + }, + { + id: "zip", + ...question.zip, + label: question.zip.placeholder[languageCode], + }, + { + id: "country", + ...question.country, + label: question.country.placeholder[languageCode], + }, + ], + [ + question.addressLine1, + question.addressLine2, + question.city, + question.state, + question.zip, + question.country, + languageCode, + ] + ); const handleChange = (fieldId: string, fieldValue: string) => { const newValue = fields.map((field) => { @@ -102,6 +116,25 @@ export function AddressQuestion({ [question.id, autoFocusEnabled, currentQuestionId] ); + const isFieldRequired = useCallback( + (field: (typeof fields)[number]) => { + if (field.required) { + return true; + } + + // if all fields are optional and the question is required, then the fields should be required + if ( + fields.filter((currField) => currField.show).every((currField) => !currField.required) && + question.required + ) { + return true; + } + + return false; + }, + [fields, question.required] + ); + return (
@@ -118,32 +151,17 @@ export function AddressQuestion({
{fields.map((field, index) => { - const isFieldRequired = () => { - if (field.required) { - return true; - } - - // if all fields are optional and the question is required, then the fields should be required - if ( - fields.filter((currField) => currField.show).every((currField) => !currField.required) && - question.required - ) { - return true; - } - - return false; - }; + const isRequired = isFieldRequired(field); return ( field.show && ( -
-