Compare commits

..

1 Commits

Author SHA1 Message Date
Cursor Agent
7f175945e8 fix: wrap custom inline scripts in try-catch to prevent runtime errors 2026-01-21 21:22:04 +00:00
34 changed files with 145 additions and 407 deletions

View File

@@ -1194,7 +1194,6 @@ checksums:
environments/surveys/edit/darken_or_lighten_background_of_your_choice: 304a64a8050ebf501d195e948cd25b6f
environments/surveys/edit/date_format: e95dfc41ac944874868487457ddc057a
environments/surveys/edit/days_before_showing_this_survey_again: 9ee757e5c3a07844b12ceb406dc65b04
environments/surveys/edit/delete_anyways: cc8683ab625280eefc9776bd381dc2e8
environments/surveys/edit/delete_block: c00617cb0724557e486304276063807a
environments/surveys/edit/delete_choice: fd750208d414b9ad8c980c161a0199e1
environments/surveys/edit/disable_the_visibility_of_survey_progress: 2af631010114307ac2a91612559c9618
@@ -1396,8 +1395,7 @@ checksums:
environments/surveys/edit/question_deleted: ecdeb22b81ae2d732656a7742c1eec7b
environments/surveys/edit/question_duplicated: 3f02439fd0a8b818bc84c1b1b473898c
environments/surveys/edit/question_id_updated: e8d94dbefcbad00c7464b3d1fb0ee81a
environments/surveys/edit/question_used_in_logic_warning_text: ec78767a7cf335222d41b98cb5baa6be
environments/surveys/edit/question_used_in_logic_warning_title: 4bb8528cdc3b8649c194487067737f6d
environments/surveys/edit/question_used_in_logic: cd1fab1a4ccdea83c6d630a59cdc9931
environments/surveys/edit/question_used_in_quota: 311b93fcecd68a65fdefbea13bec7350
environments/surveys/edit/question_used_in_recall: 00d74a1ede4e75e32d50fe87b85d5a8b
environments/surveys/edit/question_used_in_recall_ending_card: ab5b0dc296cecd160a6406cbfab42695
@@ -1569,7 +1567,6 @@ checksums:
environments/surveys/edit/validation_rules_description: a0a7cee05e18efd462148698e3a93399
environments/surveys/edit/variable_is_used_in_logic_of_question_please_remove_it_from_logic_first: bd9d9c7cf0be671c4e8cf67e2ae6659e
environments/surveys/edit/variable_is_used_in_quota_please_remove_it_from_quota_first: 0d36e5b2713f5450fe346e0af0aaa29c
environments/surveys/edit/variable_name_conflicts_with_hidden_field: fe2f6a711d5b663790bdd5780ad77bf2
environments/surveys/edit/variable_name_is_already_taken_please_choose_another: 6da42fe8733c6379158bce9a176f76d7
environments/surveys/edit/variable_name_must_start_with_a_letter: f7abbdecf1ba7b822ccabb16981ebcb5
environments/surveys/edit/variable_used_in_recall: 1c9c354a1233408cc42922eefaa8ce23

View File

@@ -1265,7 +1265,6 @@
"darken_or_lighten_background_of_your_choice": "Hintergrund deiner Wahl abdunkeln oder aufhellen.",
"date_format": "Datumsformat",
"days_before_showing_this_survey_again": "oder mehr Tage müssen zwischen der zuletzt angezeigten Umfrage und der Anzeige dieser Umfrage vergehen.",
"delete_anyways": "Trotzdem löschen",
"delete_block": "Block löschen",
"delete_choice": "Auswahl löschen",
"disable_the_visibility_of_survey_progress": "Deaktiviere die Sichtbarkeit des Umfragefortschritts.",
@@ -1467,8 +1466,7 @@
"question_deleted": "Frage gelöscht.",
"question_duplicated": "Frage dupliziert.",
"question_id_updated": "Frage-ID aktualisiert",
"question_used_in_logic_warning_text": "Elemente aus diesem Block werden in einer Logikregel verwendet. Möchten Sie ihn wirklich löschen?",
"question_used_in_logic_warning_title": "Logikinkonsistenz",
"question_used_in_logic": "Diese Frage wird in der Logik der Frage {questionIndex} verwendet.",
"question_used_in_quota": "Diese Frage wird in der \"{quotaName}\" Quote verwendet",
"question_used_in_recall": "Diese Frage wird in Frage {questionIndex} abgerufen.",
"question_used_in_recall_ending_card": "Diese Frage wird in der Abschlusskarte abgerufen.",
@@ -1644,7 +1642,6 @@
"validation_rules_description": "Nur Antworten akzeptieren, die die folgenden Kriterien erfüllen",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} wird in der Logik der Frage {questionIndex} verwendet. Bitte entferne es zuerst aus der Logik.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variable \"{variableName}\" wird in der \"{quotaName}\" Quote verwendet",
"variable_name_conflicts_with_hidden_field": "Der Variablenname steht im Konflikt mit einer vorhandenen Hidden-Field-ID.",
"variable_name_is_already_taken_please_choose_another": "Variablenname ist bereits vergeben, bitte wähle einen anderen.",
"variable_name_must_start_with_a_letter": "Variablenname muss mit einem Buchstaben beginnen.",
"variable_used_in_recall": "Variable \"{variable}\" wird in Frage {questionIndex} abgerufen.",

View File

@@ -1265,7 +1265,6 @@
"darken_or_lighten_background_of_your_choice": "Darken or lighten background of your choice.",
"date_format": "Date format",
"days_before_showing_this_survey_again": "or more days to pass between the last shown survey and showing this survey.",
"delete_anyways": "Delete anyways",
"delete_block": "Delete block",
"delete_choice": "Delete choice",
"disable_the_visibility_of_survey_progress": "Disable the visibility of survey progress.",
@@ -1467,8 +1466,7 @@
"question_deleted": "Question deleted.",
"question_duplicated": "Question duplicated.",
"question_id_updated": "Question ID updated",
"question_used_in_logic_warning_text": "Elements from this block are used in a logic rule, are you sure you want to delete it?",
"question_used_in_logic_warning_title": "Logic Inconsistency",
"question_used_in_logic": "This question is used in logic of question {questionIndex}.",
"question_used_in_quota": "This question is being used in \"{quotaName}\" quota",
"question_used_in_recall": "This question is being recalled in question {questionIndex}.",
"question_used_in_recall_ending_card": "This question is being recalled in Ending Card",
@@ -1644,7 +1642,6 @@
"validation_rules_description": "Only accept responses that meet the following criteria",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} is used in logic of question {questionIndex}. Please remove it from logic first.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variable \"{variableName}\" is being used in \"{quotaName}\" quota",
"variable_name_conflicts_with_hidden_field": "Variable name conflicts with an existing hidden field ID.",
"variable_name_is_already_taken_please_choose_another": "Variable name is already taken, please choose another.",
"variable_name_must_start_with_a_letter": "Variable name must start with a letter.",
"variable_used_in_recall": "Variable \"{variable}\" is being recalled in question {questionIndex}.",

View File

@@ -1265,7 +1265,6 @@
"darken_or_lighten_background_of_your_choice": "Oscurece o aclara el fondo de tu elección.",
"date_format": "Formato de fecha",
"days_before_showing_this_survey_again": "o más días deben transcurrir entre la última encuesta mostrada y la visualización de esta encuesta.",
"delete_anyways": "Eliminar de todos modos",
"delete_block": "Eliminar bloque",
"delete_choice": "Eliminar opción",
"disable_the_visibility_of_survey_progress": "Desactivar la visibilidad del progreso de la encuesta.",
@@ -1467,8 +1466,7 @@
"question_deleted": "Pregunta eliminada.",
"question_duplicated": "Pregunta duplicada.",
"question_id_updated": "ID de pregunta actualizado",
"question_used_in_logic_warning_text": "Los elementos de este bloque se usan en una regla de lógica, ¿estás seguro de que quieres eliminarlo?",
"question_used_in_logic_warning_title": "Inconsistencia de lógica",
"question_used_in_logic": "Esta pregunta se utiliza en la lógica de la pregunta {questionIndex}.",
"question_used_in_quota": "Esta pregunta se está utilizando en la cuota \"{quotaName}\"",
"question_used_in_recall": "Esta pregunta se está recordando en la pregunta {questionIndex}.",
"question_used_in_recall_ending_card": "Esta pregunta se está recordando en la Tarjeta Final",
@@ -1644,7 +1642,6 @@
"validation_rules_description": "Solo aceptar respuestas que cumplan los siguientes criterios",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} se usa en la lógica de la pregunta {questionIndex}. Por favor, elimínala primero de la lógica.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "La variable \"{variableName}\" se está utilizando en la cuota \"{quotaName}\"",
"variable_name_conflicts_with_hidden_field": "El nombre de la variable entra en conflicto con un ID de campo oculto existente.",
"variable_name_is_already_taken_please_choose_another": "El nombre de la variable ya está en uso, por favor elige otro.",
"variable_name_must_start_with_a_letter": "El nombre de la variable debe comenzar con una letra.",
"variable_used_in_recall": "La variable \"{variable}\" se está recuperando en la pregunta {questionIndex}.",

View File

@@ -1265,7 +1265,6 @@
"darken_or_lighten_background_of_your_choice": "Assombrir ou éclaircir l'arrière-plan de votre choix.",
"date_format": "Format de date",
"days_before_showing_this_survey_again": "ou plus de jours doivent s'écouler entre le dernier sondage affiché et l'affichage de ce sondage.",
"delete_anyways": "Supprimer quand même",
"delete_block": "Supprimer le bloc",
"delete_choice": "Supprimer l'option",
"disable_the_visibility_of_survey_progress": "Désactiver la visibilité de la progression du sondage.",
@@ -1467,8 +1466,7 @@
"question_deleted": "Question supprimée.",
"question_duplicated": "Question dupliquée.",
"question_id_updated": "ID de la question mis à jour",
"question_used_in_logic_warning_text": "Des éléments de ce bloc sont utilisés dans une règle logique, êtes-vous sûr de vouloir le supprimer?",
"question_used_in_logic_warning_title": "Incohérence de logique",
"question_used_in_logic": "Cette question est utilisée dans la logique de la question '{'questionIndex'}'.",
"question_used_in_quota": "Cette question est utilisée dans le quota \"{quotaName}\"",
"question_used_in_recall": "Cette question est rappelée dans la question {questionIndex}.",
"question_used_in_recall_ending_card": "Cette question est rappelée dans la carte de fin.",
@@ -1644,7 +1642,6 @@
"validation_rules_description": "Accepter uniquement les réponses qui répondent aux critères suivants",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} est utilisé dans la logique de la question {questionIndex}. Veuillez d'abord le supprimer de la logique.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "La variable \"{variableName}\" est utilisée dans le quota \"{quotaName}\"",
"variable_name_conflicts_with_hidden_field": "Le nom de la variable est en conflit avec un ID de champ masqué existant.",
"variable_name_is_already_taken_please_choose_another": "Le nom de la variable est déjà pris, veuillez en choisir un autre.",
"variable_name_must_start_with_a_letter": "Le nom de la variable doit commencer par une lettre.",
"variable_used_in_recall": "La variable \"{variable}\" est rappelée dans la question {questionIndex}.",

View File

@@ -1265,7 +1265,6 @@
"darken_or_lighten_background_of_your_choice": "お好みの背景を暗くしたり明るくしたりします。",
"date_format": "日付形式",
"days_before_showing_this_survey_again": "最後に表示されたアンケートとこのアンケートを表示するまでに、この日数以上の期間を空ける必要があります。",
"delete_anyways": "削除する",
"delete_block": "ブロックを削除",
"delete_choice": "選択肢を削除",
"disable_the_visibility_of_survey_progress": "フォームの進捗状況の表示を無効にする。",
@@ -1467,8 +1466,7 @@
"question_deleted": "質問を削除しました。",
"question_duplicated": "質問を複製しました。",
"question_id_updated": "質問IDを更新しました",
"question_used_in_logic_warning_text": "このブロックの要素はロジックルールで使用されていますが、本当に削除しますか?",
"question_used_in_logic_warning_title": "ロジックの不整合",
"question_used_in_logic": "この質問は質問 {questionIndex} のロジックで使用されています",
"question_used_in_quota": "この 質問 は \"{quotaName}\" の クオータ に使用されています",
"question_used_in_recall": "この 質問 は 質問 {questionIndex} で 呼び出され て います 。",
"question_used_in_recall_ending_card": "この 質問 は エンディング カード で 呼び出され て います。",
@@ -1644,7 +1642,6 @@
"validation_rules_description": "次の条件を満たす回答のみを受け付ける",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} は質問 {questionIndex} のロジックで使用されています。まず、ロジックから削除してください。",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "変数 \"{variableName}\" は \"{quotaName}\" クォータ で使用されています",
"variable_name_conflicts_with_hidden_field": "変数名が既存の非表示フィールドIDと競合しています。",
"variable_name_is_already_taken_please_choose_another": "変数名はすでに使用されています。別の名前を選択してください。",
"variable_name_must_start_with_a_letter": "変数名はアルファベットで始まらなければなりません。",
"variable_used_in_recall": "変数 \"{variable}\" が 質問 {questionIndex} で 呼び出され て います 。",

View File

@@ -1265,7 +1265,6 @@
"darken_or_lighten_background_of_your_choice": "Maak de achtergrond naar keuze donkerder of lichter.",
"date_format": "Datumformaat",
"days_before_showing_this_survey_again": "of meer dagen moeten verstrijken tussen de laatst getoonde enquête en het tonen van deze enquête.",
"delete_anyways": "Toch verwijderen",
"delete_block": "Blok verwijderen",
"delete_choice": "Keuze verwijderen",
"disable_the_visibility_of_survey_progress": "Schakel de zichtbaarheid van de voortgang van het onderzoek uit.",
@@ -1467,8 +1466,7 @@
"question_deleted": "Vraag verwijderd.",
"question_duplicated": "Vraag dubbel gesteld.",
"question_id_updated": "Vraag-ID bijgewerkt",
"question_used_in_logic_warning_text": "Elementen uit dit blok worden gebruikt in een logische regel, weet je zeker dat je het wilt verwijderen?",
"question_used_in_logic_warning_title": "Logica-inconsistentie",
"question_used_in_logic": "Deze vraag wordt gebruikt in de logica van vraag {questionIndex}.",
"question_used_in_quota": "Deze vraag wordt gebruikt in het quotum '{quotaName}'",
"question_used_in_recall": "Deze vraag wordt teruggehaald in vraag {questionIndex}.",
"question_used_in_recall_ending_card": "Deze vraag wordt teruggeroepen in de Eindkaart",
@@ -1644,7 +1642,6 @@
"validation_rules_description": "Accepteer alleen antwoorden die voldoen aan de volgende criteria",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} wordt gebruikt in de logica van vraag {questionIndex}. Verwijder het eerst uit de logica.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variabele \"{variableName}\" wordt gebruikt in het \"{quotaName}\" quotum",
"variable_name_conflicts_with_hidden_field": "Variabelenaam conflicteert met een bestaande verborgen veld-ID.",
"variable_name_is_already_taken_please_choose_another": "Variabelenaam is al in gebruik, kies een andere.",
"variable_name_must_start_with_a_letter": "Variabelenaam moet beginnen met een letter.",
"variable_used_in_recall": "Variabele \"{variable}\" wordt opgeroepen in vraag {questionIndex}.",

View File

@@ -1265,7 +1265,6 @@
"darken_or_lighten_background_of_your_choice": "Escureça ou clareie o fundo da sua escolha.",
"date_format": "Formato de data",
"days_before_showing_this_survey_again": "ou mais dias devem passar entre a última pesquisa exibida e a exibição desta pesquisa.",
"delete_anyways": "Excluir mesmo assim",
"delete_block": "Excluir bloco",
"delete_choice": "Deletar opção",
"disable_the_visibility_of_survey_progress": "Desativar a visibilidade do progresso da pesquisa.",
@@ -1467,8 +1466,7 @@
"question_deleted": "Pergunta deletada.",
"question_duplicated": "Pergunta duplicada.",
"question_id_updated": "ID da pergunta atualizado",
"question_used_in_logic_warning_text": "Elementos deste bloco são usados em uma regra de lógica, tem certeza de que deseja excluí-lo?",
"question_used_in_logic_warning_title": "Inconsistência de lógica",
"question_used_in_logic": "Essa pergunta é usada na lógica da pergunta {questionIndex}.",
"question_used_in_quota": "Esta questão está sendo usada na cota \"{quotaName}\"",
"question_used_in_recall": "Esta pergunta está sendo recordada na pergunta {questionIndex}.",
"question_used_in_recall_ending_card": "Esta pergunta está sendo recordada no card de Encerramento",
@@ -1644,7 +1642,6 @@
"validation_rules_description": "Aceitar apenas respostas que atendam aos seguintes critérios",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} está sendo usado na lógica da pergunta {questionIndex}. Por favor, remova-o da lógica primeiro.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variável \"{variableName}\" está sendo usada na cota \"{quotaName}\"",
"variable_name_conflicts_with_hidden_field": "O nome da variável está em conflito com um ID de campo oculto existente.",
"variable_name_is_already_taken_please_choose_another": "O nome da variável já está em uso, por favor escolha outro.",
"variable_name_must_start_with_a_letter": "O nome da variável deve começar com uma letra.",
"variable_used_in_recall": "Variável \"{variable}\" está sendo recordada na pergunta {questionIndex}.",

View File

@@ -1265,7 +1265,6 @@
"darken_or_lighten_background_of_your_choice": "Escurecer ou clarear o fundo da sua escolha.",
"date_format": "Formato da data",
"days_before_showing_this_survey_again": "ou mais dias a decorrer entre o último inquérito apresentado e a apresentação deste inquérito.",
"delete_anyways": "Eliminar mesmo assim",
"delete_block": "Eliminar bloco",
"delete_choice": "Eliminar escolha",
"disable_the_visibility_of_survey_progress": "Desativar a visibilidade do progresso da pesquisa.",
@@ -1467,8 +1466,7 @@
"question_deleted": "Pergunta eliminada.",
"question_duplicated": "Pergunta duplicada.",
"question_id_updated": "ID da pergunta atualizado",
"question_used_in_logic_warning_text": "Os elementos deste bloco são utilizados numa regra de lógica, tem a certeza de que pretende eliminá-lo?",
"question_used_in_logic_warning_title": "Inconsistência de lógica",
"question_used_in_logic": "Esta pergunta é usada na lógica da pergunta {questionIndex}.",
"question_used_in_quota": "Esta pergunta está a ser usada na quota \"{quotaName}\"",
"question_used_in_recall": "Esta pergunta está a ser recordada na pergunta {questionIndex}.",
"question_used_in_recall_ending_card": "Esta pergunta está a ser recordada no Cartão de Conclusão",
@@ -1644,7 +1642,6 @@
"validation_rules_description": "Aceitar apenas respostas que cumpram os seguintes critérios",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} é usada na lógica da pergunta {questionIndex}. Por favor, remova-a da lógica primeiro.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variável \"{variableName}\" está a ser utilizada na quota \"{quotaName}\"",
"variable_name_conflicts_with_hidden_field": "O nome da variável está em conflito com um ID de campo oculto existente.",
"variable_name_is_already_taken_please_choose_another": "O nome da variável já está em uso, por favor escolha outro.",
"variable_name_must_start_with_a_letter": "O nome da variável deve começar com uma letra.",
"variable_used_in_recall": "Variável \"{variable}\" está a ser recordada na pergunta {questionIndex}.",

View File

@@ -1265,7 +1265,6 @@
"darken_or_lighten_background_of_your_choice": "Întunecați sau luminați fundalul după preferințe.",
"date_format": "Format dată",
"days_before_showing_this_survey_again": "sau mai multe zile să treacă între ultima afișare a sondajului și afișarea acestui sondaj.",
"delete_anyways": "Șterge oricum",
"delete_block": "Șterge blocul",
"delete_choice": "Șterge alegerea",
"disable_the_visibility_of_survey_progress": "Dezactivați vizibilitatea progresului sondajului",
@@ -1467,8 +1466,7 @@
"question_deleted": "Întrebare ștearsă.",
"question_duplicated": "Întrebare duplicată.",
"question_id_updated": "ID întrebare actualizat",
"question_used_in_logic_warning_text": "Elemente din acest bloc sunt folosite într-o regulă de logică. Sigur doriți să îl ștergeți?",
"question_used_in_logic_warning_title": "Inconsistență logică",
"question_used_in_logic": "Această întrebare este folosită în logica întrebării {questionIndex}.",
"question_used_in_quota": "Întrebarea aceasta este folosită în cota \"{quotaName}\"",
"question_used_in_recall": "Această întrebare este reamintită în întrebarea {questionIndex}.",
"question_used_in_recall_ending_card": "Această întrebare este reamintită în Cardul de Încheiere.",
@@ -1644,7 +1642,6 @@
"validation_rules_description": "Acceptă doar răspunsurile care îndeplinesc următoarele criterii",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} este folosit în logica întrebării {questionIndex}. Vă rugăm să-l eliminați din logică mai întâi.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variabila \"{variableName}\" este folosită în cota \"{quotaName}\"",
"variable_name_conflicts_with_hidden_field": "Numele variabilei intră în conflict cu un ID de câmp ascuns existent.",
"variable_name_is_already_taken_please_choose_another": "Numele variabilei este deja utilizat, vă rugăm să alegeți altul.",
"variable_name_must_start_with_a_letter": "Numele variabilei trebuie să înceapă cu o literă.",
"variable_used_in_recall": "Variabila \"{variable}\" este reamintită în întrebarea {questionIndex}.",

View File

@@ -1265,7 +1265,6 @@
"darken_or_lighten_background_of_your_choice": "Затемните или осветлите выбранный фон.",
"date_format": "Формат даты",
"days_before_showing_this_survey_again": "или больше дней должно пройти между последним показом опроса и показом этого опроса.",
"delete_anyways": "Удалить в любом случае",
"delete_block": "Удалить блок",
"delete_choice": "Удалить вариант",
"disable_the_visibility_of_survey_progress": "Отключить отображение прогресса опроса.",
@@ -1467,8 +1466,7 @@
"question_deleted": "Вопрос удалён.",
"question_duplicated": "Вопрос дублирован.",
"question_id_updated": "ID вопроса обновлён",
"question_used_in_logic_warning_text": "Элементы из этого блока используются в правиле логики. Вы уверены, что хотите удалить его?",
"question_used_in_logic_warning_title": "Несогласованность логики",
"question_used_in_logic": "Этот вопрос используется в логике вопроса {questionIndex}.",
"question_used_in_quota": "Этот вопрос используется в квоте \"{quotaName}\"",
"question_used_in_recall": "Этот вопрос используется в отзыве в вопросе {questionIndex}.",
"question_used_in_recall_ending_card": "Этот вопрос используется в отзыве на финальной карточке",
@@ -1644,7 +1642,6 @@
"validation_rules_description": "Принимать только ответы, соответствующие следующим критериям",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} используется в логике вопроса {questionIndex}. Пожалуйста, сначала удалите его из логики.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Переменная «{variableName}» используется в квоте «{quotaName}»",
"variable_name_conflicts_with_hidden_field": "Имя переменной конфликтует с существующим ID скрытого поля.",
"variable_name_is_already_taken_please_choose_another": "Это имя переменной уже занято, выберите другое.",
"variable_name_must_start_with_a_letter": "Имя переменной должно начинаться с буквы.",
"variable_used_in_recall": "Переменная «{variable}» используется в вопросе {questionIndex}.",

View File

@@ -1265,7 +1265,6 @@
"darken_or_lighten_background_of_your_choice": "Gör bakgrunden mörkare eller ljusare efter eget val.",
"date_format": "Datumformat",
"days_before_showing_this_survey_again": "eller fler dagar måste gå mellan den senaste visade enkäten och att visa denna enkät.",
"delete_anyways": "Ta bort ändå",
"delete_block": "Ta bort block",
"delete_choice": "Ta bort val",
"disable_the_visibility_of_survey_progress": "Inaktivera synligheten av enkätens framsteg.",
@@ -1467,8 +1466,7 @@
"question_deleted": "Fråga borttagen.",
"question_duplicated": "Fråga duplicerad.",
"question_id_updated": "Fråge-ID uppdaterat",
"question_used_in_logic_warning_text": "Element från det här blocket används i en logikregel. Är du säker på att du vill ta bort det?",
"question_used_in_logic_warning_title": "Logikkonflikt",
"question_used_in_logic": "Denna fråga används i logiken för fråga {questionIndex}.",
"question_used_in_quota": "Denna fråga används i kvoten \"{quotaName}\"",
"question_used_in_recall": "Denna fråga återkallas i fråga {questionIndex}.",
"question_used_in_recall_ending_card": "Denna fråga återkallas i avslutningskortet",
@@ -1644,7 +1642,6 @@
"validation_rules_description": "Acceptera endast svar som uppfyller följande kriterier",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "{variable} används i logiken för fråga {questionIndex}. Vänligen ta bort den från logiken först.",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "Variabel \"{variableName}\" används i kvoten \"{quotaName}\"",
"variable_name_conflicts_with_hidden_field": "Variabelnamnet krockar med ett befintligt dolt fält-ID.",
"variable_name_is_already_taken_please_choose_another": "Variabelnamnet är redan taget, vänligen välj ett annat.",
"variable_name_must_start_with_a_letter": "Variabelnamnet måste börja med en bokstav.",
"variable_used_in_recall": "Variabel \"{variable}\" återkallas i fråga {questionIndex}.",

View File

@@ -1265,7 +1265,6 @@
"darken_or_lighten_background_of_your_choice": "根据 您 的 选择 暗化 或 亮化 背景。",
"date_format": "日期格式",
"days_before_showing_this_survey_again": "距离上次显示问卷后需间隔不少于指定天数,才能再次显示此问卷。",
"delete_anyways": "仍然删除",
"delete_block": "删除区块",
"delete_choice": "删除 选择",
"disable_the_visibility_of_survey_progress": "禁用问卷 进度 的可见性。",
@@ -1467,8 +1466,7 @@
"question_deleted": "问题 已删除",
"question_duplicated": "问题重复。",
"question_id_updated": "问题 ID 更新",
"question_used_in_logic_warning_text": "此区块中的元素已被用于逻辑规则,您确定要删除吗?",
"question_used_in_logic_warning_title": "逻辑不一致",
"question_used_in_logic": "\"这个 问题 在 问题 {questionIndex} 的 逻辑 中 使用。\"",
"question_used_in_quota": "此 问题 正在 被 \"{quotaName}\" 配额 使用",
"question_used_in_recall": "此问题正在召回于问题 {questionIndex}。",
"question_used_in_recall_ending_card": "此 问题 正在召回于结束 卡片。",
@@ -1644,7 +1642,6 @@
"validation_rules_description": "仅接受符合以下条件的回复",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "\"{variable} 在 问题 {questionIndex} 的 逻辑 中 使用。请 先 从 逻辑 中 删除 它。\"",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "变量 \"{variableName}\" 正在 被 \"{quotaName}\" 配额 使用",
"variable_name_conflicts_with_hidden_field": "变量名与已有的隐藏字段 ID 冲突。",
"variable_name_is_already_taken_please_choose_another": "变量名已被占用,请选择其他。",
"variable_name_must_start_with_a_letter": "变量名 必须 以字母开头。",
"variable_used_in_recall": "变量 \"{variable}\" 正在召回于问题 {questionIndex}。",

View File

@@ -1265,7 +1265,6 @@
"darken_or_lighten_background_of_your_choice": "變暗或變亮您選擇的背景。",
"date_format": "日期格式",
"days_before_showing_this_survey_again": "距離上次顯示問卷後,需間隔指定天數才能再次顯示此問卷。",
"delete_anyways": "仍要刪除",
"delete_block": "刪除區塊",
"delete_choice": "刪除選項",
"disable_the_visibility_of_survey_progress": "停用問卷進度的可見性。",
@@ -1467,8 +1466,7 @@
"question_deleted": "問題已刪除。",
"question_duplicated": "問題已複製。",
"question_id_updated": "問題 ID 已更新",
"question_used_in_logic_warning_text": "此區塊中的元素已用於邏輯規則,確定要刪除嗎?",
"question_used_in_logic_warning_title": "邏輯不一致",
"question_used_in_logic": "此問題用於問題 '{'questionIndex'}' 的邏輯中。",
"question_used_in_quota": "此問題 正被使用於 \"{quotaName}\" 配額中",
"question_used_in_recall": "此問題於問題 {questionIndex} 中被召回。",
"question_used_in_recall_ending_card": "此問題於結尾卡中被召回。",
@@ -1644,7 +1642,6 @@
"validation_rules_description": "僅接受符合下列條件的回應",
"variable_is_used_in_logic_of_question_please_remove_it_from_logic_first": "'{'variable'}' 用於問題 '{'questionIndex'}' 的邏輯中。請先從邏輯中移除。",
"variable_is_used_in_quota_please_remove_it_from_quota_first": "變數 \"{variableName}\" 正被使用於 \"{quotaName}\" 配額中",
"variable_name_conflicts_with_hidden_field": "變數名稱與現有的隱藏欄位 ID 衝突。",
"variable_name_is_already_taken_please_choose_another": "已使用此變數名稱,請選擇另一個名稱。",
"variable_name_must_start_with_a_letter": "變數名稱必須以字母開頭。",
"variable_used_in_recall": "變數 \"{variable}\" 於問題 {questionIndex} 中被召回。",

View File

@@ -3,7 +3,6 @@
import { CheckCircle2Icon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { TResponseWithQuotas } from "@formbricks/types/responses";
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
import { TSurvey } from "@formbricks/types/surveys/types";
import { getTextContent } from "@formbricks/types/surveys/validation";
import { getLocalizedValue } from "@/lib/i18n/utils";
@@ -68,16 +67,6 @@ export const SingleResponseCardBody = ({
<VerifiedEmail responseData={response.data} />
)}
{elements.map((question) => {
// Skip CTA elements without external buttons only if they have no response data
// This preserves historical data from when buttonExternal was true
if (
question.type === TSurveyElementTypeEnum.CTA &&
!question.buttonExternal &&
!response.data[question.id]
) {
return null;
}
const skipped = skippedQuestions.find((skippedQuestionElement) =>
skippedQuestionElement.includes(question.id)
);

View File

@@ -8,7 +8,6 @@ import {
getBiggerUploadFileSizePermission,
getIsContactsEnabled,
getIsMultiOrgEnabled,
getIsQuotasEnabled,
getIsSamlSsoEnabled,
getIsSpamProtectionEnabled,
getIsSsoEnabled,
@@ -49,7 +48,6 @@ const defaultFeatures: TEnterpriseLicenseFeatures = {
auditLogs: false,
multiLanguageSurveys: false,
accessControl: false,
quotas: false,
};
const defaultLicense = {
@@ -186,10 +184,10 @@ describe("License Utils", () => {
expect(result).toBe(true);
});
test("should return false if license active but accessControl feature disabled (self-hosted)", async () => {
test("should return true if license active but accessControl feature disabled because of fallback", async () => {
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
const result = await getAccessControlPermission(mockOrganization.billing.plan);
expect(result).toBe(false);
expect(result).toBe(true);
});
test("should return false if license is inactive", async () => {
@@ -275,10 +273,10 @@ describe("License Utils", () => {
expect(result).toBe(true);
});
test("should return false if license active but multiLanguageSurveys feature disabled (self-hosted)", async () => {
test("should return true if license active but multiLanguageSurveys feature disabled because of fallback", async () => {
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
const result = await getMultiLanguagePermission(mockOrganization.billing.plan);
expect(result).toBe(false);
expect(result).toBe(true);
});
test("should return false if license is inactive", async () => {
@@ -291,54 +289,6 @@ describe("License Utils", () => {
});
});
describe("getIsQuotasEnabled", () => {
test("should return true if license active and quotas feature enabled (self-hosted)", async () => {
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
...defaultLicense,
features: { ...defaultFeatures, quotas: true },
});
const result = await getIsQuotasEnabled(mockOrganization.billing.plan);
expect(result).toBe(true);
});
test("should return true if license active, quotas enabled and plan is CUSTOM (cloud)", async () => {
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
...defaultLicense,
features: { ...defaultFeatures, quotas: true },
});
const result = await getIsQuotasEnabled(constants.PROJECT_FEATURE_KEYS.CUSTOM);
expect(result).toBe(true);
});
test("should return false if license active, quotas enabled but plan is not CUSTOM (cloud)", async () => {
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
...defaultLicense,
features: { ...defaultFeatures, quotas: true },
});
const result = await getIsQuotasEnabled(constants.PROJECT_FEATURE_KEYS.STARTUP);
expect(result).toBe(false);
});
test("should return false if license active but quotas feature disabled (self-hosted)", async () => {
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
const result = await getIsQuotasEnabled(mockOrganization.billing.plan);
expect(result).toBe(false);
});
test("should return false if license is inactive", async () => {
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
...defaultLicense,
active: false,
});
const result = await getIsQuotasEnabled(mockOrganization.billing.plan);
expect(result).toBe(false);
});
});
describe("getIsMultiOrgEnabled", () => {
test("should return true if feature flag isMultiOrgEnabled is true", async () => {
vi.mocked(licenseModule.getLicenseFeatures).mockResolvedValue({

View File

@@ -10,8 +10,6 @@ import { TEnterpriseLicenseFeatures } from "@/modules/ee/license-check/types/ent
import { getEnterpriseLicense, getLicenseFeatures } from "./license";
// Helper function for feature permissions (e.g., removeBranding, whitelabel)
// On Cloud: requires active license and non-FREE plan
// On Self-hosted: requires active license and feature enabled
const getFeaturePermission = async (
billingPlan: Organization["billing"]["plan"],
featureKey: keyof Pick<TEnterpriseLicenseFeatures, "removeBranding" | "whitelabel">
@@ -25,41 +23,6 @@ const getFeaturePermission = async (
}
};
// Helper function for enterprise features that require CUSTOM plan on Cloud
// On Cloud: requires active license AND feature enabled in license AND CUSTOM billing plan
// On Self-hosted: requires active license AND feature enabled in license
const getCustomPlanFeaturePermission = async (
billingPlan: Organization["billing"]["plan"],
featureKey: keyof Pick<TEnterpriseLicenseFeatures, "accessControl" | "multiLanguageSurveys" | "quotas">
): Promise<boolean> => {
const license = await getEnterpriseLicense();
if (!license.active) return false;
const isFeatureEnabled = license.features?.[featureKey] ?? false;
if (!isFeatureEnabled) return false;
if (IS_FORMBRICKS_CLOUD) {
return billingPlan === PROJECT_FEATURE_KEYS.CUSTOM;
}
return true;
};
// Helper function for license-only feature flags (no billing plan check)
// Returns true only if the license is active AND the specific feature is enabled in the license
// Used for features that are controlled purely by the license key, not billing plans
const getSpecificFeatureFlag = async (
featureKey: keyof Pick<
TEnterpriseLicenseFeatures,
"isMultiOrgEnabled" | "contacts" | "twoFactorAuth" | "sso" | "auditLogs"
>
): Promise<boolean> => {
const licenseFeatures = await getLicenseFeatures();
if (!licenseFeatures) return false;
return typeof licenseFeatures[featureKey] === "boolean" ? licenseFeatures[featureKey] : false;
};
export const getRemoveBrandingPermission = async (
billingPlan: Organization["billing"]["plan"]
): Promise<boolean> => {
@@ -82,6 +45,24 @@ export const getBiggerUploadFileSizePermission = async (
return false;
};
const getSpecificFeatureFlag = async (
featureKey: keyof Pick<
TEnterpriseLicenseFeatures,
| "isMultiOrgEnabled"
| "contacts"
| "twoFactorAuth"
| "sso"
| "auditLogs"
| "multiLanguageSurveys"
| "accessControl"
| "quotas"
>
): Promise<boolean> => {
const licenseFeatures = await getLicenseFeatures();
if (!licenseFeatures) return false;
return typeof licenseFeatures[featureKey] === "boolean" ? licenseFeatures[featureKey] : false;
};
export const getIsMultiOrgEnabled = async (): Promise<boolean> => {
return getSpecificFeatureFlag("isMultiOrgEnabled");
};
@@ -99,7 +80,12 @@ export const getIsSsoEnabled = async (): Promise<boolean> => {
};
export const getIsQuotasEnabled = async (billingPlan: Organization["billing"]["plan"]): Promise<boolean> => {
return getCustomPlanFeaturePermission(billingPlan, "quotas");
const isEnabled = await getSpecificFeatureFlag("quotas");
// If the feature is enabled in the license, return true
if (isEnabled) return true;
// If the feature is not enabled in the license, check the fallback(Backwards compatibility)
return featureFlagFallback(billingPlan);
};
export const getIsAuditLogsEnabled = async (): Promise<boolean> => {
@@ -132,16 +118,33 @@ export const getIsSpamProtectionEnabled = async (
return license.active && !!license.features?.spamProtection;
};
const featureFlagFallback = async (billingPlan: Organization["billing"]["plan"]): Promise<boolean> => {
const license = await getEnterpriseLicense();
if (IS_FORMBRICKS_CLOUD) return license.active && billingPlan === PROJECT_FEATURE_KEYS.CUSTOM;
else if (!IS_FORMBRICKS_CLOUD) return license.active;
return false;
};
export const getMultiLanguagePermission = async (
billingPlan: Organization["billing"]["plan"]
): Promise<boolean> => {
return getCustomPlanFeaturePermission(billingPlan, "multiLanguageSurveys");
const isEnabled = await getSpecificFeatureFlag("multiLanguageSurveys");
// If the feature is enabled in the license, return true
if (isEnabled) return true;
// If the feature is not enabled in the license, check the fallback(Backwards compatibility)
return featureFlagFallback(billingPlan);
};
export const getAccessControlPermission = async (
billingPlan: Organization["billing"]["plan"]
): Promise<boolean> => {
return getCustomPlanFeaturePermission(billingPlan, "accessControl");
const isEnabled = await getSpecificFeatureFlag("accessControl");
// If the feature is enabled in the license, return true
if (isEnabled) return true;
// If the feature is not enabled in the license, check the fallback(Backwards compatibility)
return featureFlagFallback(billingPlan);
};
export const getOrganizationProjectsLimit = async (

View File

@@ -46,14 +46,8 @@ import {
renumberBlocks,
updateElementInBlock,
} from "@/modules/survey/editor/lib/blocks";
import {
findBlockUsedInLogic,
findElementUsedInLogic,
isUsedInQuota,
isUsedInRecall,
} from "@/modules/survey/editor/lib/utils";
import { findElementUsedInLogic, isUsedInQuota, isUsedInRecall } from "@/modules/survey/editor/lib/utils";
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal";
import { isEndingCardValid, isWelcomeCardValid, validateSurveyElementsInBatch } from "../lib/validation";
interface ElementsViewProps {
@@ -100,16 +94,6 @@ export const ElementsView = ({
isExternalUrlsAllowed,
}: ElementsViewProps) => {
const { t } = useTranslation();
const [logicDeletionWarning, setLogicDeletionWarning] = React.useState<{
open: boolean;
elementIdx: number;
type: "element" | "block";
blockId?: string;
}>({
open: false,
elementIdx: 0,
type: "element",
});
const elements = useMemo(() => getElementsFromBlocks(localSurvey.blocks), [localSurvey.blocks]);
@@ -404,6 +388,14 @@ export const ElementsView = ({
};
const validateElementDeletion = (elementId: string, elementIdx: number): boolean => {
const usedElementIdx = findElementUsedInLogic(localSurvey, elementId);
if (usedElementIdx !== -1) {
toast.error(
t("environments.surveys.edit.question_used_in_logic", { questionIndex: usedElementIdx + 1 })
);
return false;
}
const recallElementIdx = isUsedInRecall(localSurvey, elementId);
if (recallElementIdx === elements.length) {
toast.error(t("environments.surveys.edit.question_used_in_recall_ending_card"));
@@ -447,11 +439,15 @@ export const ElementsView = ({
}
};
const executeDeletion = (elementIdx: number) => {
const deleteElement = (elementIdx: number) => {
const element = elements[elementIdx];
if (!element) return;
const elementId = element.id;
if (!validateElementDeletion(elementId, elementIdx)) {
return;
}
const activeElementIdTemp = activeElementId ?? elements[0]?.id;
// let updatedSurvey = removeRecallReferences(localSurvey, elementId);
let updatedSurvey = structuredClone(localSurvey);
@@ -479,24 +475,6 @@ export const ElementsView = ({
toast.success(t("environments.surveys.edit.question_deleted"));
};
const deleteElement = (elementIdx: number) => {
const element = elements[elementIdx];
if (!element) return;
const elementId = element.id;
if (!validateElementDeletion(elementId, elementIdx)) {
return;
}
const usedElementIdx = findElementUsedInLogic(localSurvey, elementId);
if (usedElementIdx !== -1) {
setLogicDeletionWarning({ open: true, elementIdx, type: "element" });
return;
}
executeDeletion(elementIdx);
};
const duplicateElement = (elementIdx: number) => {
const element = elements[elementIdx];
if (!element) return;
@@ -694,7 +672,7 @@ export const ElementsView = ({
toast.success(t("environments.surveys.edit.block_duplicated"));
};
const executeBlockDeletion = (blockId: string) => {
const deleteBlockById = (blockId: string) => {
// First check if block exists in current state (for validation and calculating next active element)
const blockExists = localSurvey.blocks.some((b) => b.id === blockId);
if (!blockExists) {
@@ -731,28 +709,6 @@ export const ElementsView = ({
}
};
const deleteBlockById = (blockId: string) => {
// First check if block is used in logic
const usedElementIdx = findBlockUsedInLogic(localSurvey, blockId);
if (usedElementIdx !== -1) {
setLogicDeletionWarning({ open: true, elementIdx: 0, type: "block", blockId });
return;
}
// Then check if any element in the block is used in recall/quota
const block = localSurvey.blocks.find((b) => b.id === blockId);
if (block) {
for (const element of block.elements) {
const elementIdx = elements.findIndex((e) => e.id === element.id);
if (!validateElementDeletion(element.id, elementIdx)) {
return;
}
}
}
executeBlockDeletion(blockId);
};
const moveBlockById = (blockId: string, direction: "up" | "down") => {
const result = moveBlockHelper(localSurvey, blockId, direction);
@@ -962,22 +918,6 @@ export const ElementsView = ({
</>
)}
</div>
<ConfirmationModal
open={logicDeletionWarning.open}
setOpen={(open) => setLogicDeletionWarning((prev) => ({ ...prev, open: open as boolean }))}
title={t("environments.surveys.edit.question_used_in_logic_warning_title")}
body={t("environments.surveys.edit.question_used_in_logic_warning_text")}
buttonText={t("environments.surveys.edit.delete_anyways")}
onConfirm={() => {
if (logicDeletionWarning.type === "element") {
executeDeletion(logicDeletionWarning.elementIdx);
} else if (logicDeletionWarning.type === "block" && logicDeletionWarning.blockId) {
executeBlockDeletion(logicDeletionWarning.blockId);
}
setLogicDeletionWarning((prev) => ({ ...prev, open: false }));
}}
/>
</div>
);
};

View File

@@ -203,14 +203,12 @@ export const HiddenFieldsCard = ({
const existingElementIds = elements.map((element) => element.id);
const existingEndingCardIds = localSurvey.endings.map((ending) => ending.id);
const existingHiddenFieldIds = localSurvey.hiddenFields.fieldIds ?? [];
const existingVariableNames = localSurvey.variables.map((v) => v.name);
const validateIdError = validateId(
"Hidden field",
hiddenField,
existingElementIds,
existingEndingCardIds,
existingHiddenFieldIds,
existingVariableNames
existingHiddenFieldIds
);
if (validateIdError) {

View File

@@ -425,19 +425,11 @@ export const SurveyMenuBar = ({
const segment = await handleSegmentUpdate();
clearSurveyLocalStorage();
const publishResult = await updateSurveyAction({
await updateSurveyAction({
...localSurvey,
status,
segment,
});
if (!publishResult?.data) {
const errorMessage = getFormattedErrorMessage(publishResult);
toast.error(errorMessage);
setIsSurveyPublishing(false);
return;
}
setIsSurveyPublishing(false);
// Set flag to prevent beforeunload warning during navigation
isSuccessfullySavedRef.current = true;
@@ -475,7 +467,7 @@ export const SurveyMenuBar = ({
/>
</div>
<div className="mt-3 flex items-center gap-2 sm:ml-4 sm:mt-0">
<div className="mt-3 flex items-center gap-2 sm:mt-0 sm:ml-4">
<AutoSaveIndicator isDraft={localSurvey.status === "draft"} lastSaved={lastAutoSaved} />
{!isStorageConfigured && (
<div>

View File

@@ -186,10 +186,6 @@ export const SurveyVariablesCardItem = ({
if (!/^[a-z]/.test(value)) {
return t("environments.surveys.edit.variable_name_must_start_with_a_letter");
}
const hiddenFieldIds = localSurvey.hiddenFields?.fieldIds ?? [];
if (hiddenFieldIds.some((id) => id.toLowerCase() === value.toLowerCase())) {
return t("environments.surveys.edit.variable_name_conflicts_with_hidden_field");
}
},
}}
render={({ field }) => (

View File

@@ -1300,14 +1300,6 @@ export const findElementUsedInLogic = (survey: TSurvey, elementId: string): numb
return true;
}
if (
action.objective === "calculate" &&
action.value.type === "element" &&
action.value.value === elementId
) {
return true;
}
return action.objective === "jumpToBlock" && action.target === block.id;
};
@@ -1330,45 +1322,6 @@ export const findElementUsedInLogic = (survey: TSurvey, elementId: string): numb
});
};
export const findBlockUsedInLogic = (survey: TSurvey, blockId: string): number => {
const targetBlock = survey.blocks.find((b) => b.id === blockId);
if (!targetBlock) return -1;
const isUsedInAction = (action: TSurveyBlockLogicAction): boolean => {
return action.objective === "jumpToBlock" && action.target === blockId;
};
const isUsedInLogicRule = (logicRule: TSurveyBlockLogic): boolean => {
return logicRule.actions.some(isUsedInAction);
};
const elements = getElementsFromBlocks(survey.blocks);
const blockUsageIndex = elements.findIndex((element) => {
const { block } = findElementLocation(survey, element.id);
if (!block) {
return false;
}
return block.id !== blockId && (block.logic?.some(isUsedInLogicRule) || block.logicFallback === blockId);
});
if (blockUsageIndex !== -1) {
return blockUsageIndex;
}
// Check if any element in the block is used in logic
for (const element of targetBlock.elements) {
const elementUsedIndex = findElementUsedInLogic(survey, element.id);
if (elementUsedIndex !== -1) {
return elementUsedIndex;
}
}
return -1;
};
export const isUsedInQuota = (
quota: TSurveyQuota,
{

View File

@@ -56,9 +56,19 @@ export const CustomScriptsInjector = ({
newScript.setAttribute(attr.name, attr.value);
});
// Copy inline script content
// Copy inline script content and wrap in try-catch to prevent runtime errors
if (script.textContent) {
newScript.textContent = script.textContent;
// Wrap user scripts in try-catch to prevent errors like missing browser globals
// from breaking the survey (e.g., ReferenceError: xbrowser is not defined)
newScript.textContent = `
(function() {
try {
${script.textContent}
} catch (error) {
console.warn("[Formbricks] Error executing custom script:", error);
}
})();
`.trim();
}
document.head.appendChild(newScript);

View File

@@ -259,7 +259,6 @@ export const PreviewSurvey = ({
setBlockId = f;
}}
onFinished={onFinished}
placement={placement}
isSpamProtectionEnabled={isSpamProtectionEnabled}
/>
</Modal>
@@ -364,7 +363,6 @@ export const PreviewSurvey = ({
}}
onFinished={onFinished}
isSpamProtectionEnabled={isSpamProtectionEnabled}
placement={placement}
/>
</Modal>
) : (

View File

@@ -170,7 +170,7 @@ export const getLanguageCode = (survey: TEnvironmentStateSurvey, language?: stri
const selectedLanguage = survey.languages.find((surveyLanguage) => {
return (
surveyLanguage.language.code.toLowerCase() === language.toLowerCase() ||
surveyLanguage.language.code === language.toLowerCase() ||
surveyLanguage.language.alias?.toLowerCase() === language.toLowerCase()
);
});

View File

@@ -2,7 +2,7 @@ import * as React from "react";
import { ElementError } from "@/components/general/element-error";
import { ElementHeader } from "@/components/general/element-header";
import { Label } from "@/components/general/label";
import { cn, getRTLScaleOptionClasses } from "@/lib/utils";
import { cn } from "@/lib/utils";
interface NPSProps {
/** Unique identifier for the element container */
@@ -97,9 +97,18 @@ function NPS({
const isLast = number === 10; // Last option is 10
const isFirst = number === 0; // First option is 0
// Use CSS logical properties for RTL-aware borders and border radius
// The fieldset's dir attribute automatically handles direction
const { borderRadiusClasses, borderClasses } = getRTLScaleOptionClasses(isFirst, isLast);
// Determine border radius and border classes
// Use right border for all items to create separators, left border only on first item
let borderRadiusClasses = "";
let borderClasses = "border-t border-b border-r";
if (isFirst) {
borderRadiusClasses = dir === "rtl" ? "rounded-r-input" : "rounded-l-input";
borderClasses = "border-t border-b border-l border-r";
} else if (isLast) {
borderRadiusClasses = dir === "rtl" ? "rounded-l-input" : "rounded-r-input";
// Last item keeps right border for rounded corner
}
return (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- label is interactive
@@ -174,7 +183,7 @@ function NPS({
{/* NPS Options */}
<div className="relative">
<ElementError errorMessage={errorMessage} dir={dir} />
<fieldset className="w-full px-[2px]" dir={dir}>
<fieldset className="w-full px-[2px]">
<legend className="sr-only">NPS rating options</legend>
<div className="flex w-full">{npsOptions.map((number) => renderNPSOption(number))}</div>

View File

@@ -15,7 +15,7 @@ import {
TiredFace,
WearyFace,
} from "@/components/general/smileys";
import { cn, getRTLScaleOptionClasses } from "@/lib/utils";
import { cn } from "@/lib/utils";
/**
* Get smiley color class based on range and index
@@ -220,9 +220,18 @@ function Rating({
const isLast = totalLength === number;
const isFirst = number === 1;
// Use CSS logical properties for RTL-aware borders and border radius
// The parent div's dir attribute automatically handles direction
const { borderRadiusClasses, borderClasses } = getRTLScaleOptionClasses(isFirst, isLast);
// Determine border radius and border classes
// Use right border for all items to create separators, left border only on first item
let borderRadiusClasses = "";
let borderClasses = "border-t border-b border-r";
if (isFirst) {
borderRadiusClasses = dir === "rtl" ? "rounded-r-input" : "rounded-l-input";
borderClasses = "border-t border-b border-l border-r";
} else if (isLast) {
borderRadiusClasses = dir === "rtl" ? "rounded-l-input" : "rounded-r-input";
// Last item keeps right border for rounded corner
}
return (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- label is interactive
@@ -409,7 +418,7 @@ function Rating({
{/* Rating Options */}
<div className="relative">
<ElementError errorMessage={errorMessage} dir={dir} />
<fieldset className="w-full" dir={dir}>
<fieldset className="w-full">
<legend className="sr-only">Rating options</legend>
<div className="flex w-full px-[2px]">
{ratingOptions.map((number, index) => {

View File

@@ -35,29 +35,3 @@ export const stripInlineStyles = (html: string): string => {
KEEP_CONTENT: true,
});
};
/**
* Generate RTL-aware border radius and border classes for rating/NPS scale options
* Uses CSS logical properties that automatically adapt to text direction
* @param isFirst - Whether this is the first item in the scale
* @param isLast - Whether this is the last item in the scale
* @returns Object containing borderRadiusClasses and borderClasses
*/
export const getRTLScaleOptionClasses = (
isFirst: boolean,
isLast: boolean
): { borderRadiusClasses: string; borderClasses: string } => {
const borderRadiusClasses = cn(
isFirst &&
"[border-start-start-radius:var(--fb-input-border-radius)] [border-end-start-radius:var(--fb-input-border-radius)]",
isLast &&
"[border-start-end-radius:var(--fb-input-border-radius)] [border-end-end-radius:var(--fb-input-border-radius)]"
);
const borderClasses = cn(
"border-t border-b border-e", // block borders (top/bottom) and inline-end border
isFirst && "border-s" // inline-start border for first item
);
return { borderRadiusClasses, borderClasses };
};

View File

@@ -89,12 +89,7 @@ export function EndingCard({
useEffect(() => {
if (isCurrent) {
if (
!isRedirectDisabled &&
endingCard.type === "redirectToUrl" &&
endingCard.url &&
isResponseSendingFinished
) {
if (!isRedirectDisabled && endingCard.type === "redirectToUrl" && endingCard.url) {
processAndRedirect(endingCard.url);
}
}
@@ -114,7 +109,9 @@ export function EndingCard({
return () => {
document.removeEventListener("keydown", handleEnter);
};
}, [isCurrent, isResponseSendingFinished, isRedirectDisabled, endingCard, survey.type]);
// eslint-disable-next-line react-hooks/exhaustive-deps -- we only want to run this effect when isCurrent changes
}, [isCurrent]);
return (
<ScrollableContainer fullSizeCards={fullSizeCards}>

View File

@@ -76,7 +76,6 @@ export function Survey({
isSpamProtectionEnabled,
dir = "auto",
setDir,
placement,
}: SurveyContainerProps) {
let apiClient: ApiClient | null = null;
@@ -917,7 +916,6 @@ export function Survey({
setBlockId={setBlockId}
shouldResetBlockId={shouldResetQuestionId}
fullSizeCards={fullSizeCards}
placement={placement}
/>
);
}

View File

@@ -2,7 +2,6 @@ import { MutableRef } from "preact/hooks";
import { useEffect, useMemo, useState } from "preact/hooks";
import { JSX } from "preact/jsx-runtime";
import React from "react";
import { type TPlacement } from "@formbricks/types/common";
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
import { TCardArrangementOptions } from "@formbricks/types/styling";
@@ -18,7 +17,6 @@ interface StackedCardProps {
cardWidth: number;
hovered: boolean;
cardArrangement: TCardArrangementOptions;
placement: TPlacement;
}
export const StackedCard = ({
@@ -33,24 +31,17 @@ export const StackedCard = ({
cardWidth,
hovered,
cardArrangement,
placement,
}: StackedCardProps) => {
const isHidden = offset < 0;
const [delayedOffset, setDelayedOffset] = useState<number>(offset);
const [contentOpacity, setContentOpacity] = useState<number>(0);
const currentCardHeight = offset === 0 ? "auto" : offset < 0 ? "initial" : cardHeight;
const getTopBottomStyles = () => {
const getBottomStyles = () => {
if (survey.type !== "link")
if (placement === "bottomLeft" || placement === "bottomRight") {
return {
bottom: 0,
};
} else if (placement === "topLeft" || placement === "topRight") {
return {
top: 0,
};
}
return {
bottom: 0,
};
};
const getDummyCardContent = () => {
@@ -120,7 +111,7 @@ export const StackedCard = ({
pointerEvents: offset === 0 ? "auto" : "none",
...borderStyles,
...straightCardArrangementStyles,
...getTopBottomStyles(),
...getBottomStyles(),
}}
className="pointer rounded-custom bg-survey-bg absolute inset-x-0 overflow-hidden">
<div

View File

@@ -1,6 +1,5 @@
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import type { JSX } from "react";
import { type TPlacement } from "@formbricks/types/common";
import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js";
import { type TProjectStyling } from "@formbricks/types/project";
import { type TCardArrangementOptions } from "@formbricks/types/styling";
@@ -20,7 +19,6 @@ interface StackedCardsContainerProps {
setBlockId: (blockId: string) => void;
shouldResetBlockId?: boolean;
fullSizeCards: boolean;
placement?: TPlacement;
}
export function StackedCardsContainer({
@@ -32,7 +30,6 @@ export function StackedCardsContainer({
setBlockId,
shouldResetBlockId = true,
fullSizeCards = false,
placement = "bottomRight",
}: Readonly<StackedCardsContainerProps>) {
const [hovered, setHovered] = useState(false);
const highlightBorderColor = survey.styling?.overwriteThemeStyling
@@ -143,9 +140,9 @@ export function StackedCardsContainer({
}, [cardArrangement]);
return (
<div // NOSONAR - hover handlers are for visual feedback on card animation, not interactive content
<div
data-testid="stacked-cards-container"
className="relative flex h-full items-center justify-center"
className="relative flex h-full items-end justify-center md:items-center"
onMouseEnter={() => {
setHovered(true);
}}
@@ -182,7 +179,6 @@ export function StackedCardsContainer({
cardWidth={cardWidth}
hovered={hovered}
cardArrangement={cardArrangement}
placement={placement}
/>
);
})

View File

@@ -36,35 +36,18 @@ export const renderSurvey = (props: SurveyContainerProps) => {
throw new Error(`renderSurvey: Element with id ${containerId} not found.`);
}
// if survey type is link, we don't pass the placement, darkOverlay, clickOutside, onClose
if (props.survey.type === "link") {
const { placement, darkOverlay, onClose, clickOutside, ...surveyInlineProps } = props;
const { placement, darkOverlay, onClose, clickOutside, ...surveyInlineProps } = props;
render(
h(
I18nProvider,
{ language },
h(RenderSurvey, {
...surveyInlineProps,
})
),
element
);
} else {
// For non-link surveys, pass placement through so it can be used in StackedCard
const { darkOverlay, onClose, clickOutside, ...surveyInlineProps } = props;
render(
h(
I18nProvider,
{ language },
h(RenderSurvey, {
...surveyInlineProps,
})
),
element
);
}
render(
h(
I18nProvider,
{ language },
h(RenderSurvey, {
...surveyInlineProps,
})
),
element
);
} else {
const modalContainer = document.createElement("div");
modalContainer.id = "formbricks-modal-container";

View File

@@ -304,22 +304,16 @@ export const validateId = (
field: string,
existingElementIds: string[],
existingEndingCardIds: string[],
existingHiddenFieldIds: string[],
existingVariableNames: string[] = []
existingHiddenFieldIds: string[]
): string | null => {
if (field.trim() === "") {
return `Please enter a ${type} Id.`;
}
const combinedIds = [
...existingElementIds,
...existingHiddenFieldIds,
...existingEndingCardIds,
...existingVariableNames,
];
const combinedIds = [...existingElementIds, ...existingHiddenFieldIds, ...existingEndingCardIds];
if (combinedIds.findIndex((id) => id.toLowerCase() === field.toLowerCase()) !== -1) {
return `${type} ID already exists in questions, hidden fields, or variables`;
return `${type} ID already exists in questions or hidden fields`;
}
if (FORBIDDEN_IDS.includes(field)) {