mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-10 02:24:17 -05:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5fbc6317d | |||
| b951fbcbc8 |
@@ -2212,6 +2212,7 @@
|
||||
"custom_scripts_warning": "Skripte werden mit vollem Browser-Zugriff ausgeführt. Fügen Sie nur Skripte aus vertrauenswürdigen Quellen hinzu.",
|
||||
"delete_workspace": "Projekt löschen",
|
||||
"delete_workspace_confirmation": "Sind Sie sicher, dass Sie {projectName} löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"delete_workspace_confirmation_name": "Bitte gib {projectName} in das folgende Feld ein, um die endgültige Löschung dieses Projekts zu bestätigen:",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "{projectName} inkl. aller Umfragen, Antworten, Personen, Aktionen und Attribute löschen.",
|
||||
"delete_workspace_settings_description": "Projekt mit allen Umfragen, Antworten, Personen, Aktionen und Attributen löschen. Das kann nicht rückgängig gemacht werden.",
|
||||
"error_saving_workspace_information": "Fehler beim Speichern der Projektinformationen",
|
||||
@@ -2429,11 +2430,13 @@
|
||||
"s": {
|
||||
"check_inbox_or_spam": "Bitte überprüfe auch deinen Spam-Ordner, falls Du die E-Mail nicht in deinem Posteingang siehst.",
|
||||
"completed": "Diese kostenlose und quelloffene Umfrage wurde geschlossen.",
|
||||
"completed_heading": "Abgeschlossen",
|
||||
"create_your_own": "Erstelle deine eigene",
|
||||
"enter_pin": "Diese Umfrage ist geschützt. Gib die PIN unten ein.",
|
||||
"just_curious": "Einfach neugierig?",
|
||||
"link_invalid": "Diese Umfrage kann nur auf Einladung durchgeführt werden.",
|
||||
"paused": "Diese Umfrage ist vorübergehend pausiert.",
|
||||
"paused_heading": "Pausiert",
|
||||
"please_try_again_with_the_original_link": "Bitte versuche es nochmal mit dem ursprünglichen Link",
|
||||
"preview_survey_questions": "Vorschau der Fragen.",
|
||||
"question_preview": "Vorschau der Frage",
|
||||
@@ -2447,9 +2450,7 @@
|
||||
"verify_email_before_submission": "Bestätige deine E-Mail, um zu antworten",
|
||||
"verify_email_before_submission_button": "Überprüfen",
|
||||
"verify_email_before_submission_description": "Um an dieser Umfrage teilzunehmen, bitte bestätige deine E-Mail",
|
||||
"want_to_respond": "Möchtest Du antworten?",
|
||||
"paused_heading": "Pausiert",
|
||||
"completed_heading": "Abgeschlossen"
|
||||
"want_to_respond": "Möchtest Du antworten?"
|
||||
},
|
||||
"setup": {
|
||||
"intro": {
|
||||
|
||||
@@ -2212,6 +2212,7 @@
|
||||
"custom_scripts_warning": "Scripts execute with full browser access. Only add scripts from trusted sources.",
|
||||
"delete_workspace": "Delete Workspace",
|
||||
"delete_workspace_confirmation": "Are you sure you want to delete {projectName}? This action cannot be undone.",
|
||||
"delete_workspace_confirmation_name": "Please enter {projectName} in the following field to confirm the definitive deletion of this workspace:",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Delete {projectName} including all surveys, responses, people, actions and attributes.",
|
||||
"delete_workspace_settings_description": "Delete workspace with all surveys, responses, people, actions and attributes. This cannot be undone.",
|
||||
"error_saving_workspace_information": "Error saving workspace information",
|
||||
@@ -2429,11 +2430,13 @@
|
||||
"s": {
|
||||
"check_inbox_or_spam": "Please also check your spam folder if you do not see the email in your inbox.",
|
||||
"completed": "This survey is closed.",
|
||||
"completed_heading": "Completed",
|
||||
"create_your_own": "Create your own open-source survey",
|
||||
"enter_pin": "This survey is protected. Enter the PIN below.",
|
||||
"just_curious": "Just curious?",
|
||||
"link_invalid": "This survey can only be taken by invitation.",
|
||||
"paused": "This survey is temporarily paused.",
|
||||
"paused_heading": "Paused",
|
||||
"please_try_again_with_the_original_link": "Please try again with the original link",
|
||||
"preview_survey_questions": "Preview survey questions.",
|
||||
"question_preview": "Question Preview",
|
||||
@@ -2447,9 +2450,7 @@
|
||||
"verify_email_before_submission": "Verify your email to respond",
|
||||
"verify_email_before_submission_button": "Verify",
|
||||
"verify_email_before_submission_description": "To respond to this survey, please verify your email",
|
||||
"want_to_respond": "Want to respond?",
|
||||
"paused_heading": "Paused",
|
||||
"completed_heading": "Completed"
|
||||
"want_to_respond": "Want to respond?"
|
||||
},
|
||||
"setup": {
|
||||
"intro": {
|
||||
|
||||
@@ -2212,6 +2212,7 @@
|
||||
"custom_scripts_warning": "Los scripts se ejecutan con acceso completo al navegador. Solo añade scripts de fuentes confiables.",
|
||||
"delete_workspace": "Eliminar proyecto",
|
||||
"delete_workspace_confirmation": "¿Estás seguro de que quieres eliminar {projectName}? Esta acción no se puede deshacer.",
|
||||
"delete_workspace_confirmation_name": "Por favor, introduce {projectName} en el siguiente campo para confirmar la eliminación definitiva de este proyecto:",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Eliminar {projectName} incluyendo todas las encuestas, respuestas, personas, acciones y atributos.",
|
||||
"delete_workspace_settings_description": "Eliminar proyecto con todas las encuestas, respuestas, personas, acciones y atributos. Esto no se puede deshacer.",
|
||||
"error_saving_workspace_information": "Error al guardar la información del proyecto",
|
||||
@@ -2429,11 +2430,13 @@
|
||||
"s": {
|
||||
"check_inbox_or_spam": "Por favor, comprueba también tu carpeta de spam si no ves el correo electrónico en tu bandeja de entrada.",
|
||||
"completed": "Esta encuesta está cerrada.",
|
||||
"completed_heading": "Completado",
|
||||
"create_your_own": "Crea tu propia encuesta de código abierto",
|
||||
"enter_pin": "Esta encuesta está protegida. Introduce el PIN a continuación.",
|
||||
"just_curious": "¿Solo tienes curiosidad?",
|
||||
"link_invalid": "Esta encuesta solo se puede realizar por invitación.",
|
||||
"paused": "Esta encuesta está temporalmente pausada.",
|
||||
"paused_heading": "Pausado",
|
||||
"please_try_again_with_the_original_link": "Por favor, inténtalo de nuevo con el enlace original",
|
||||
"preview_survey_questions": "Vista previa de las preguntas de la encuesta.",
|
||||
"question_preview": "Vista previa de la pregunta",
|
||||
@@ -2447,9 +2450,7 @@
|
||||
"verify_email_before_submission": "Verifica tu correo electrónico para responder",
|
||||
"verify_email_before_submission_button": "Verificar",
|
||||
"verify_email_before_submission_description": "Para responder a esta encuesta, por favor verifica tu correo electrónico",
|
||||
"want_to_respond": "¿Quieres responder?",
|
||||
"paused_heading": "Pausado",
|
||||
"completed_heading": "Completado"
|
||||
"want_to_respond": "¿Quieres responder?"
|
||||
},
|
||||
"setup": {
|
||||
"intro": {
|
||||
|
||||
@@ -2212,6 +2212,7 @@
|
||||
"custom_scripts_warning": "Les scripts s'exécutent avec un accès complet au navigateur. Ajoutez uniquement des scripts provenant de sources fiables.",
|
||||
"delete_workspace": "Supprimer le projet",
|
||||
"delete_workspace_confirmation": "Êtes-vous sûr de vouloir supprimer {projectName} ? Cette action ne peut pas être annulée.",
|
||||
"delete_workspace_confirmation_name": "Veuillez entrer {projectName} dans le champ suivant pour confirmer la suppression définitive de ce projet :",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Supprimer {projectName} y compris toutes les enquêtes, réponses, personnes, actions et attributs.",
|
||||
"delete_workspace_settings_description": "Supprimer le projet avec toutes les enquêtes, réponses, personnes, actions et attributs. Cette opération est irréversible.",
|
||||
"error_saving_workspace_information": "Erreur lors de l'enregistrement des informations du projet",
|
||||
@@ -2429,11 +2430,13 @@
|
||||
"s": {
|
||||
"check_inbox_or_spam": "Veuillez également vérifier votre dossier de spam si vous ne voyez pas l'e-mail dans votre boîte de réception.",
|
||||
"completed": "Cette enquête gratuite et open-source a été fermée.",
|
||||
"completed_heading": "Terminé",
|
||||
"create_your_own": "Créez le vôtre",
|
||||
"enter_pin": "Ce sondage est protégé. Entrez le code PIN ci-dessous.",
|
||||
"just_curious": "Juste curieux ?",
|
||||
"link_invalid": "Cette enquête ne peut être réalisée que sur invitation.",
|
||||
"paused": "Cette enquête gratuite et open-source est temporairement suspendue.",
|
||||
"paused_heading": "En pause",
|
||||
"please_try_again_with_the_original_link": "Veuillez réessayer avec le lien d'origine.",
|
||||
"preview_survey_questions": "Aperçu des questions de l'enquête.",
|
||||
"question_preview": "Aperçu de la question",
|
||||
@@ -2447,9 +2450,7 @@
|
||||
"verify_email_before_submission": "Vérifiez votre email pour répondre.",
|
||||
"verify_email_before_submission_button": "Vérifier",
|
||||
"verify_email_before_submission_description": "Pour répondre à cette enquête, veuillez vérifier votre e-mail.",
|
||||
"want_to_respond": "Voulez-vous répondre ?",
|
||||
"paused_heading": "En pause",
|
||||
"completed_heading": "Terminé"
|
||||
"want_to_respond": "Voulez-vous répondre ?"
|
||||
},
|
||||
"setup": {
|
||||
"intro": {
|
||||
|
||||
@@ -2212,6 +2212,7 @@
|
||||
"custom_scripts_warning": "A parancsfájlok teljes böngésző-hozzáféréssel kerülnek végrehajtásra. Csak megbízható forrásokból származó parancsfájlokat adjon hozzá.",
|
||||
"delete_workspace": "Munkaterület törlése",
|
||||
"delete_workspace_confirmation": "Biztosan törölni szeretné a(z) {projectName} munkaterületet? Ezt a műveletet nem lehet visszavonni.",
|
||||
"delete_workspace_confirmation_name": "Adja meg a(z) {projectName} munkaterület nevét a következő mezőben a munkaterület végleges törlésének megerősítéséhez:",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "A(z) {projectName} munkaterület törlése, beleértve az összes kérdőívet, választ, személyt, műveletet és attribútumot is.",
|
||||
"delete_workspace_settings_description": "A munkaterület törlése az összes kérdőívvel, válasszal, személlyel, művelettel és attribútummal együtt. Ezt nem lehet visszavonni.",
|
||||
"error_saving_workspace_information": "Hiba a munkaterület-információk mentésekor",
|
||||
@@ -2429,11 +2430,13 @@
|
||||
"s": {
|
||||
"check_inbox_or_spam": "Nézze meg a levélszemét mappát is, ha nem találja az e-mailt a beérkező levelek között.",
|
||||
"completed": "Ez a kérdőív le van zárva.",
|
||||
"completed_heading": "Befejezve",
|
||||
"create_your_own": "Saját nyílt forráskódú kérdőív létrehozása",
|
||||
"enter_pin": "Ez a kérdőív védett. Adja meg a PIN-kódot lent.",
|
||||
"just_curious": "Csak kíváncsi?",
|
||||
"link_invalid": "Ez a kérdőív csak meghívás útján tölthető ki.",
|
||||
"paused": "Ez a kérdőív átmenetileg szüneteltetve van.",
|
||||
"paused_heading": "Szüneteltetve",
|
||||
"please_try_again_with_the_original_link": "Próbálja meg újra az eredeti hivatkozással",
|
||||
"preview_survey_questions": "Kérdőív kérdéseinek előnézete.",
|
||||
"question_preview": "Kérdés előnézete",
|
||||
@@ -2447,9 +2450,7 @@
|
||||
"verify_email_before_submission": "Ellenőrizze az e-mail-címét a válaszadáshoz",
|
||||
"verify_email_before_submission_button": "Ellenőrzés",
|
||||
"verify_email_before_submission_description": "A kérdőívre való válaszadáshoz ellenőrizze az e-mail-címét",
|
||||
"want_to_respond": "Szeretne válaszolni?",
|
||||
"paused_heading": "Szüneteltetve",
|
||||
"completed_heading": "Befejezve"
|
||||
"want_to_respond": "Szeretne válaszolni?"
|
||||
},
|
||||
"setup": {
|
||||
"intro": {
|
||||
|
||||
@@ -2212,6 +2212,7 @@
|
||||
"custom_scripts_warning": "スクリプトはブラウザへの完全なアクセス権で実行されます。信頼できるソースからのスクリプトのみを追加してください。",
|
||||
"delete_workspace": "ワークスペースを削除",
|
||||
"delete_workspace_confirmation": "{projectName}を削除してもよろしいですか?このアクションは元に戻せません。",
|
||||
"delete_workspace_confirmation_name": "このワークスペースの完全な削除を確認するには、以下のフィールドに {projectName} と入力してください:",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "{projectName}をすべてのフォーム、回答、人物、アクション、属性を含めて削除します。",
|
||||
"delete_workspace_settings_description": "すべてのフォーム、回答、人物、アクション、属性を含むワークスペースを削除します。この操作は元に戻せません。",
|
||||
"error_saving_workspace_information": "ワークスペース情報の保存中にエラーが発生しました",
|
||||
@@ -2429,11 +2430,13 @@
|
||||
"s": {
|
||||
"check_inbox_or_spam": "受信トレイにメールがない場合は、迷惑メールフォルダも確認してください。",
|
||||
"completed": "このフォームはクローズしました。",
|
||||
"completed_heading": "完了",
|
||||
"create_your_own": "独自のオープンソースフォームを作成",
|
||||
"enter_pin": "このアンケートは保護されています。以下にPINを入力してください。",
|
||||
"just_curious": "ただ興味があるだけですか?",
|
||||
"link_invalid": "このフォームは招待によってのみ回答できます。",
|
||||
"paused": "このフォームは一時的に一時停止されています。",
|
||||
"paused_heading": "一時停止",
|
||||
"please_try_again_with_the_original_link": "元のリンクでもう一度お試しください",
|
||||
"preview_survey_questions": "フォームの質問をプレビュー。",
|
||||
"question_preview": "質問プレビュー",
|
||||
@@ -2447,9 +2450,7 @@
|
||||
"verify_email_before_submission": "回答するにはメールアドレスを認証してください",
|
||||
"verify_email_before_submission_button": "認証",
|
||||
"verify_email_before_submission_description": "このフォームに回答するには、メールアドレスを認証してください",
|
||||
"want_to_respond": "回答しますか?",
|
||||
"paused_heading": "一時停止",
|
||||
"completed_heading": "完了"
|
||||
"want_to_respond": "回答しますか?"
|
||||
},
|
||||
"setup": {
|
||||
"intro": {
|
||||
|
||||
@@ -2212,6 +2212,7 @@
|
||||
"custom_scripts_warning": "Scripts worden uitgevoerd met volledige browsertoegang. Voeg alleen scripts toe van vertrouwde bronnen.",
|
||||
"delete_workspace": "Project verwijderen",
|
||||
"delete_workspace_confirmation": "Weet u zeker dat u {projectName} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||
"delete_workspace_confirmation_name": "Voer {projectName} in het volgende veld in om de definitieve verwijdering van dit project te bevestigen:",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Verwijder {projectName} incl. alle enquêtes, reacties, mensen, acties en attributen.",
|
||||
"delete_workspace_settings_description": "Verwijder project met alle enquêtes, reacties, mensen, acties en attributen. Dit kan niet ongedaan worden gemaakt.",
|
||||
"error_saving_workspace_information": "Fout bij opslaan van projectinformatie",
|
||||
@@ -2429,11 +2430,13 @@
|
||||
"s": {
|
||||
"check_inbox_or_spam": "Controleer ook uw spammap als u de e-mail niet in uw inbox ziet.",
|
||||
"completed": "Deze enquête is gesloten.",
|
||||
"completed_heading": "Voltooid",
|
||||
"create_your_own": "Creëer uw eigen open source-enquête",
|
||||
"enter_pin": "Deze enquête is beveiligd. Voer hieronder de pincode in.",
|
||||
"just_curious": "Gewoon nieuwsgierig?",
|
||||
"link_invalid": "Aan deze enquête kan alleen op uitnodiging worden deelgenomen.",
|
||||
"paused": "Deze enquête is tijdelijk onderbroken.",
|
||||
"paused_heading": "Gepauzeerd",
|
||||
"please_try_again_with_the_original_link": "Probeer het opnieuw met de originele link",
|
||||
"preview_survey_questions": "Bekijk enquêtevragen.",
|
||||
"question_preview": "Vraagvoorbeeld",
|
||||
@@ -2447,9 +2450,7 @@
|
||||
"verify_email_before_submission": "Verifieer uw e-mailadres om te reageren",
|
||||
"verify_email_before_submission_button": "Verifiëren",
|
||||
"verify_email_before_submission_description": "Om op deze enquête te reageren, dient u uw e-mailadres te verifiëren",
|
||||
"want_to_respond": "Wilt u reageren?",
|
||||
"paused_heading": "Gepauzeerd",
|
||||
"completed_heading": "Voltooid"
|
||||
"want_to_respond": "Wilt u reageren?"
|
||||
},
|
||||
"setup": {
|
||||
"intro": {
|
||||
|
||||
@@ -2212,6 +2212,7 @@
|
||||
"custom_scripts_warning": "Os scripts são executados com acesso total ao navegador. Adicione apenas scripts de fontes confiáveis.",
|
||||
"delete_workspace": "Excluir projeto",
|
||||
"delete_workspace_confirmation": "Tem certeza de que deseja excluir {projectName}? Essa ação não pode ser desfeita.",
|
||||
"delete_workspace_confirmation_name": "Por favor, insira {projectName} no campo abaixo para confirmar a exclusão definitiva deste projeto:",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Excluir {projectName} incluindo todas as pesquisas, respostas, pessoas, ações e atributos.",
|
||||
"delete_workspace_settings_description": "Excluir projeto com todas as pesquisas, respostas, pessoas, ações e atributos. Isso não pode ser desfeito.",
|
||||
"error_saving_workspace_information": "Erro ao salvar informações do projeto",
|
||||
@@ -2429,11 +2430,13 @@
|
||||
"s": {
|
||||
"check_inbox_or_spam": "Por favor, dá uma olhada na sua pasta de spam se você não encontrar o e-mail na sua caixa de entrada.",
|
||||
"completed": "Essa pesquisa gratuita e de código aberto foi encerrada.",
|
||||
"completed_heading": "Concluído",
|
||||
"create_your_own": "Crie o seu próprio",
|
||||
"enter_pin": "Esta pesquisa está protegida. Digite o PIN abaixo.",
|
||||
"just_curious": "Só curioso?",
|
||||
"link_invalid": "Essa pesquisa só pode ser respondida por convite.",
|
||||
"paused": "Essa pesquisa gratuita e de código aberto está temporariamente pausada.",
|
||||
"paused_heading": "Pausado",
|
||||
"please_try_again_with_the_original_link": "Por favor, tente novamente com o link original",
|
||||
"preview_survey_questions": "Visualizar perguntas da pesquisa.",
|
||||
"question_preview": "Prévia da Pergunta",
|
||||
@@ -2447,9 +2450,7 @@
|
||||
"verify_email_before_submission": "Verifique seu e-mail para responder",
|
||||
"verify_email_before_submission_button": "Verificar",
|
||||
"verify_email_before_submission_description": "Para responder a esta pesquisa, confirme seu e-mail",
|
||||
"want_to_respond": "Quer responder?",
|
||||
"paused_heading": "Pausado",
|
||||
"completed_heading": "Concluído"
|
||||
"want_to_respond": "Quer responder?"
|
||||
},
|
||||
"setup": {
|
||||
"intro": {
|
||||
|
||||
@@ -2212,6 +2212,7 @@
|
||||
"custom_scripts_warning": "Os scripts são executados com acesso total ao navegador. Adicione apenas scripts de fontes fidedignas.",
|
||||
"delete_workspace": "Eliminar projeto",
|
||||
"delete_workspace_confirmation": "Tem a certeza de que pretende eliminar {projectName}? Esta ação não pode ser desfeita.",
|
||||
"delete_workspace_confirmation_name": "Por favor, insira {projectName} no campo seguinte para confirmar a eliminação definitiva deste projeto:",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Eliminar {projectName} incluindo todos os inquéritos, respostas, pessoas, ações e atributos.",
|
||||
"delete_workspace_settings_description": "Eliminar projeto com todos os inquéritos, respostas, pessoas, ações e atributos. Isto não pode ser desfeito.",
|
||||
"error_saving_workspace_information": "Erro ao guardar informações do projeto",
|
||||
@@ -2429,11 +2430,13 @@
|
||||
"s": {
|
||||
"check_inbox_or_spam": "Por favor, verifique também a sua pasta de spam se não vir o email na sua caixa de entrada.",
|
||||
"completed": "Este inquérito está encerrado.",
|
||||
"completed_heading": "Concluído",
|
||||
"create_your_own": "Crie o seu próprio inquérito de código aberto",
|
||||
"enter_pin": "Este inquérito está protegido. Introduza o PIN abaixo.",
|
||||
"just_curious": "Só por curiosidade?",
|
||||
"link_invalid": "Este inquérito só pode ser respondido por convite.",
|
||||
"paused": "Este inquérito está temporariamente suspenso.",
|
||||
"paused_heading": "Em pausa",
|
||||
"please_try_again_with_the_original_link": "Por favor, tente novamente com o link original",
|
||||
"preview_survey_questions": "Pré-visualizar perguntas do inquérito.",
|
||||
"question_preview": "Pré-visualização da Pergunta",
|
||||
@@ -2447,9 +2450,7 @@
|
||||
"verify_email_before_submission": "Verifique o seu email para responder",
|
||||
"verify_email_before_submission_button": "Verificar",
|
||||
"verify_email_before_submission_description": "Para responder a este questionário, por favor verifique o seu email",
|
||||
"want_to_respond": "Quer responder?",
|
||||
"paused_heading": "Em pausa",
|
||||
"completed_heading": "Concluído"
|
||||
"want_to_respond": "Quer responder?"
|
||||
},
|
||||
"setup": {
|
||||
"intro": {
|
||||
|
||||
@@ -2212,6 +2212,7 @@
|
||||
"custom_scripts_warning": "Scripturile se execută cu acces complet la browser. Adaugă doar scripturi din surse de încredere.",
|
||||
"delete_workspace": "Șterge proiectul",
|
||||
"delete_workspace_confirmation": "Sigur vrei să ștergi {projectName}? Această acțiune nu poate fi anulată.",
|
||||
"delete_workspace_confirmation_name": "Vă rugăm să introduceți {projectName} în câmpul următor pentru a confirma ștergerea definitivă a acestui proiect:",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Șterge {projectName} incl. toate sondajele, răspunsurile, persoanele, acțiunile și atributele.",
|
||||
"delete_workspace_settings_description": "Șterge proiectul cu toate sondajele, răspunsurile, persoanele, acțiunile și atributele. Aceasta nu poate fi anulată.",
|
||||
"error_saving_workspace_information": "Eroare la salvarea informațiilor despre proiect",
|
||||
@@ -2429,11 +2430,13 @@
|
||||
"s": {
|
||||
"check_inbox_or_spam": "Vă rugăm să verificați și folderul de spam dacă nu vedeți emailul în inbox.",
|
||||
"completed": "Acest chestionar este închis.",
|
||||
"completed_heading": "Completat",
|
||||
"create_your_own": "Creează-ți propriul chestionar open-source",
|
||||
"enter_pin": "Acest sondaj este protejat. Introduceți PIN-ul mai jos.",
|
||||
"just_curious": "Doar curios?",
|
||||
"link_invalid": "Acest sondaj poate fi completat doar pe bază de invitație.",
|
||||
"paused": "Acest sondaj este temporar întrerupt.",
|
||||
"paused_heading": "Pauză",
|
||||
"please_try_again_with_the_original_link": "Vă rugăm să încercați din nou cu linkul original",
|
||||
"preview_survey_questions": "Previzualizare întrebări chestionar",
|
||||
"question_preview": "Previzualizare Întrebare",
|
||||
@@ -2447,9 +2450,7 @@
|
||||
"verify_email_before_submission": "Verificați-vă emailul pentru a răspunde",
|
||||
"verify_email_before_submission_button": "Verifică",
|
||||
"verify_email_before_submission_description": "Pentru a răspunde la acest sondaj, vă rugăm să vă verificați emailul",
|
||||
"want_to_respond": "Dorești să răspunzi?",
|
||||
"paused_heading": "Pauză",
|
||||
"completed_heading": "Completat"
|
||||
"want_to_respond": "Dorești să răspunzi?"
|
||||
},
|
||||
"setup": {
|
||||
"intro": {
|
||||
|
||||
@@ -2212,6 +2212,7 @@
|
||||
"custom_scripts_warning": "Скрипты выполняются с полным доступом к браузеру. Добавляйте только скрипты из доверенных источников.",
|
||||
"delete_workspace": "Удалить рабочий проект",
|
||||
"delete_workspace_confirmation": "Вы уверены, что хотите удалить {projectName}? Это действие необратимо.",
|
||||
"delete_workspace_confirmation_name": "Пожалуйста, введите {projectName} в поле ниже для подтверждения окончательного удаления этого рабочего проекта:",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Удалить {projectName} вместе со всеми опросами, ответами, пользователями, действиями и атрибутами.",
|
||||
"delete_workspace_settings_description": "Удалить рабочий проект со всеми опросами, ответами, пользователями, действиями и атрибутами. Это действие необратимо.",
|
||||
"error_saving_workspace_information": "Ошибка при сохранении информации о рабочем проекте",
|
||||
@@ -2429,11 +2430,13 @@
|
||||
"s": {
|
||||
"check_inbox_or_spam": "Если вы не видите письмо во входящих, проверьте папку со спамом.",
|
||||
"completed": "Этот опрос закрыт.",
|
||||
"completed_heading": "Завершено",
|
||||
"create_your_own": "Создайте свой собственный опрос с открытым исходным кодом",
|
||||
"enter_pin": "Этот опрос защищён. Введите PIN ниже.",
|
||||
"just_curious": "Просто интересно?",
|
||||
"link_invalid": "Пройти этот опрос можно только по приглашению.",
|
||||
"paused": "Этот опрос временно приостановлен.",
|
||||
"paused_heading": "Приостановлено",
|
||||
"please_try_again_with_the_original_link": "Пожалуйста, попробуйте снова, используя оригинальную ссылку",
|
||||
"preview_survey_questions": "Просмотреть вопросы опроса.",
|
||||
"question_preview": "Предпросмотр вопроса",
|
||||
@@ -2447,9 +2450,7 @@
|
||||
"verify_email_before_submission": "Подтвердите свой email, чтобы ответить",
|
||||
"verify_email_before_submission_button": "Подтвердить",
|
||||
"verify_email_before_submission_description": "Чтобы ответить на этот опрос, пожалуйста, подтвердите свой email",
|
||||
"want_to_respond": "Хотите ответить?",
|
||||
"paused_heading": "Приостановлено",
|
||||
"completed_heading": "Завершено"
|
||||
"want_to_respond": "Хотите ответить?"
|
||||
},
|
||||
"setup": {
|
||||
"intro": {
|
||||
|
||||
@@ -2212,6 +2212,7 @@
|
||||
"custom_scripts_warning": "Skript körs med full åtkomst till webbläsaren. Lägg endast till skript från betrodda källor.",
|
||||
"delete_workspace": "Ta bort arbetsyta",
|
||||
"delete_workspace_confirmation": "Är du säker på att du vill ta bort {projectName}? Denna åtgärd kan inte ångras.",
|
||||
"delete_workspace_confirmation_name": "Vänligen ange {projectName} i följande fält för att bekräfta den definitiva borttagningen av denna arbetsyta:",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Ta bort {projectName} inkl. alla enkäter, svar, personer, åtgärder och attribut.",
|
||||
"delete_workspace_settings_description": "Ta bort arbetsyta med alla enkäter, svar, personer, åtgärder och attribut. Detta kan inte ångras.",
|
||||
"error_saving_workspace_information": "Fel vid sparande av arbetsytans information",
|
||||
@@ -2429,11 +2430,13 @@
|
||||
"s": {
|
||||
"check_inbox_or_spam": "Vänligen kontrollera även din skräppost om du inte ser e-postmeddelandet i din inkorg.",
|
||||
"completed": "Denna enkät är stängd.",
|
||||
"completed_heading": "Slutförd",
|
||||
"create_your_own": "Skapa din egen öppenkällkodsenkät",
|
||||
"enter_pin": "Denna undersökning är skyddad. Ange PIN-koden nedan.",
|
||||
"just_curious": "Bara nyfiken?",
|
||||
"link_invalid": "Denna enkät kan endast tas via inbjudan.",
|
||||
"paused": "Denna enkät är tillfälligt pausad.",
|
||||
"paused_heading": "Pausad",
|
||||
"please_try_again_with_the_original_link": "Vänligen försök igen med den ursprungliga länken",
|
||||
"preview_survey_questions": "Förhandsgranska enkätfrågor.",
|
||||
"question_preview": "Frågeförhandsgranskning",
|
||||
@@ -2447,9 +2450,7 @@
|
||||
"verify_email_before_submission": "Verifiera din e-post för att svara",
|
||||
"verify_email_before_submission_button": "Verifiera",
|
||||
"verify_email_before_submission_description": "För att svara på denna enkät, vänligen verifiera din e-post",
|
||||
"want_to_respond": "Vill du svara?",
|
||||
"paused_heading": "Pausad",
|
||||
"completed_heading": "Slutförd"
|
||||
"want_to_respond": "Vill du svara?"
|
||||
},
|
||||
"setup": {
|
||||
"intro": {
|
||||
|
||||
@@ -2212,6 +2212,7 @@
|
||||
"custom_scripts_warning": "Betikler tam tarayıcı erişimiyle çalışır. Yalnızca güvenilir kaynaklardan betik ekleyin.",
|
||||
"delete_workspace": "Çalışma Alanını Sil",
|
||||
"delete_workspace_confirmation": "{projectName} öğesini silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
|
||||
"delete_workspace_confirmation_name": "Bu çalışma alanının kesin olarak silinmesini onaylamak için lütfen aşağıdaki alana {projectName} yazın:",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "Tüm survey, yanıt, kişi, eylem ve nitelikleri dahil {projectName} öğesini silin.",
|
||||
"delete_workspace_settings_description": "Tüm survey, yanıt, kişi, eylem ve nitelikleriyle birlikte çalışma alanını silin. Bu işlem geri alınamaz.",
|
||||
"error_saving_workspace_information": "Çalışma alanı bilgileri kaydedilirken hata oluştu",
|
||||
@@ -2429,11 +2430,13 @@
|
||||
"s": {
|
||||
"check_inbox_or_spam": "Email'i gelen kutunuzda görmüyorsanız lütfen spam klasörünüzü de kontrol edin.",
|
||||
"completed": "Bu survey kapatılmıştır.",
|
||||
"completed_heading": "Tamamlandı",
|
||||
"create_your_own": "Kendi açık kaynak survey'inizi oluşturun",
|
||||
"enter_pin": "Bu survey korumalıdır. Aşağıya PIN girin.",
|
||||
"just_curious": "Sadece merak mı ediyorsunuz?",
|
||||
"link_invalid": "Bu survey yalnızca davet ile katılıma açıktır.",
|
||||
"paused": "Bu survey geçici olarak duraklatılmıştır.",
|
||||
"paused_heading": "Duraklatıldı",
|
||||
"please_try_again_with_the_original_link": "Lütfen orijinal bağlantıyla tekrar deneyin",
|
||||
"preview_survey_questions": "Survey sorularını önizleyin.",
|
||||
"question_preview": "Soru Önizlemesi",
|
||||
@@ -2447,9 +2450,7 @@
|
||||
"verify_email_before_submission": "Yanıtlamak için email adresinizi doğrulayın",
|
||||
"verify_email_before_submission_button": "Doğrula",
|
||||
"verify_email_before_submission_description": "Bu survey'e yanıt vermek için lütfen email adresinizi doğrulayın",
|
||||
"want_to_respond": "Yanıtlamak ister misiniz?",
|
||||
"paused_heading": "Duraklatıldı",
|
||||
"completed_heading": "Tamamlandı"
|
||||
"want_to_respond": "Yanıtlamak ister misiniz?"
|
||||
},
|
||||
"setup": {
|
||||
"intro": {
|
||||
|
||||
@@ -2212,6 +2212,7 @@
|
||||
"custom_scripts_warning": "脚本将以完整浏览器权限执行。请仅添加来自可信来源的脚本。",
|
||||
"delete_workspace": "删除工作区",
|
||||
"delete_workspace_confirmation": "您确定要删除 {projectName} 吗?此操作无法撤销。",
|
||||
"delete_workspace_confirmation_name": "请在下列字段中输入 {projectName} 以确认永久删除此工作区:",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "删除 {projectName},包括所有调查、回应、人员、动作和属性。",
|
||||
"delete_workspace_settings_description": "删除工作区及其所有调查、回应、人员、动作和属性。此操作无法撤销。",
|
||||
"error_saving_workspace_information": "保存工作区信息时出错",
|
||||
@@ -2429,11 +2430,13 @@
|
||||
"s": {
|
||||
"check_inbox_or_spam": "请 也 检查 您 的 垃圾邮件 文件夹 如果 您 没有 在 收件箱 中 看到 邮件。",
|
||||
"completed": "此 调查 关闭",
|
||||
"completed_heading": "完成",
|
||||
"create_your_own": "创建 你 的 开源 调查",
|
||||
"enter_pin": "本调查已受保护。请在下方输入PIN码。",
|
||||
"just_curious": "只是好奇 ?",
|
||||
"link_invalid": "此调查只能通过邀请参加。",
|
||||
"paused": "此调查暂时暂停。",
|
||||
"paused_heading": "暂停",
|
||||
"please_try_again_with_the_original_link": "请 尝试 使用 原始 链接",
|
||||
"preview_survey_questions": "预览 问卷调查 问题。",
|
||||
"question_preview": "问题 预览",
|
||||
@@ -2447,9 +2450,7 @@
|
||||
"verify_email_before_submission": "验证 您的 邮件 以 响应",
|
||||
"verify_email_before_submission_button": "验证",
|
||||
"verify_email_before_submission_description": "要 响应 此 调查,请 验证 您的 邮件",
|
||||
"want_to_respond": "想要 参与 吗?",
|
||||
"paused_heading": "暂停",
|
||||
"completed_heading": "完成"
|
||||
"want_to_respond": "想要 参与 吗?"
|
||||
},
|
||||
"setup": {
|
||||
"intro": {
|
||||
|
||||
@@ -2212,6 +2212,7 @@
|
||||
"custom_scripts_warning": "腳本將以完整瀏覽器權限執行。請僅加入來自可信來源的腳本。",
|
||||
"delete_workspace": "刪除工作區",
|
||||
"delete_workspace_confirmation": "您確定要刪除 {projectName} 嗎?此操作無法復原。",
|
||||
"delete_workspace_confirmation_name": "請在下列欄位中輸入 {projectName} 以確認永久刪除此工作區:",
|
||||
"delete_workspace_name_includes_surveys_responses_people_and_more": "刪除 {projectName}(包含所有問卷、回應、人員、操作和屬性)。",
|
||||
"delete_workspace_settings_description": "刪除工作區及其所有問卷、回應、人員、操作和屬性。此操作無法復原。",
|
||||
"error_saving_workspace_information": "儲存工作區資訊時發生錯誤",
|
||||
@@ -2429,11 +2430,13 @@
|
||||
"s": {
|
||||
"check_inbox_or_spam": "如果您的收件匣中沒有看到電子郵件,也請檢查您的垃圾郵件資料夾。",
|
||||
"completed": "此免費且開源的問卷已關閉。",
|
||||
"completed_heading": "已完成",
|
||||
"create_your_own": "建立您自己的",
|
||||
"enter_pin": "此問卷已受保護。請在下方輸入 PIN 碼。",
|
||||
"just_curious": "只是好奇?",
|
||||
"link_invalid": "此問卷只能透過邀請填寫。",
|
||||
"paused": "此免費且開源的問卷已暫時暫停。",
|
||||
"paused_heading": "已暫停",
|
||||
"please_try_again_with_the_original_link": "請使用原始連結再試一次",
|
||||
"preview_survey_questions": "預覽問卷問題。",
|
||||
"question_preview": "問題預覽",
|
||||
@@ -2447,9 +2450,7 @@
|
||||
"verify_email_before_submission": "驗證您的電子郵件以回應",
|
||||
"verify_email_before_submission_button": "驗證",
|
||||
"verify_email_before_submission_description": "若要回應此問卷,請驗證您的電子郵件",
|
||||
"want_to_respond": "想要回應嗎?",
|
||||
"paused_heading": "已暫停",
|
||||
"completed_heading": "已完成"
|
||||
"want_to_respond": "想要回應嗎?"
|
||||
},
|
||||
"setup": {
|
||||
"intro": {
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { deleteProjectAction } from "./actions";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
deleteProjectWithConfirmation: vi.fn(),
|
||||
getProjectIdForLogging: vi.fn(),
|
||||
loggerError: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: mocks.loggerError,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/action-client", () => ({
|
||||
authenticatedActionClient: {
|
||||
inputSchema: vi.fn(() => ({
|
||||
action: vi.fn((fn) => fn),
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ee/audit-logs/lib/handler", () => ({
|
||||
withAuditLogging: vi.fn((_eventName, _objectType, fn) => fn),
|
||||
}));
|
||||
|
||||
vi.mock("./lib/delete-project", () => ({
|
||||
deleteProjectWithConfirmation: mocks.deleteProjectWithConfirmation,
|
||||
getProjectIdForLogging: mocks.getProjectIdForLogging,
|
||||
}));
|
||||
|
||||
const baseProject = {
|
||||
id: "cmproject000000000000000000",
|
||||
name: "Acme Workspace",
|
||||
organizationId: "cmorg00000000000000000000",
|
||||
};
|
||||
|
||||
const ctx = {
|
||||
user: {
|
||||
id: "cmuser00000000000000000000",
|
||||
},
|
||||
auditLoggingCtx: {},
|
||||
};
|
||||
|
||||
describe("deleteProjectAction", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.getProjectIdForLogging.mockReturnValue(baseProject.id);
|
||||
mocks.deleteProjectWithConfirmation.mockResolvedValue(baseProject);
|
||||
});
|
||||
|
||||
test("delegates workspace deletion to the covered lib", async () => {
|
||||
const parsedInput = {
|
||||
projectId: baseProject.id,
|
||||
confirmationName: "acme workspace",
|
||||
};
|
||||
|
||||
const result = await deleteProjectAction({
|
||||
ctx,
|
||||
parsedInput,
|
||||
} as any);
|
||||
|
||||
expect(mocks.getProjectIdForLogging).toHaveBeenCalledWith(parsedInput);
|
||||
expect(mocks.deleteProjectWithConfirmation).toHaveBeenCalledWith({
|
||||
input: parsedInput,
|
||||
userId: ctx.user.id,
|
||||
auditLoggingCtx: ctx.auditLoggingCtx,
|
||||
});
|
||||
expect(result).toEqual(baseProject);
|
||||
});
|
||||
|
||||
test("logs and rethrows deletion failures", async () => {
|
||||
const error = new Error("delete failed");
|
||||
mocks.deleteProjectWithConfirmation.mockRejectedValueOnce(error);
|
||||
|
||||
await expect(
|
||||
deleteProjectAction({
|
||||
ctx,
|
||||
parsedInput: {
|
||||
projectId: baseProject.id,
|
||||
confirmationName: baseProject.name,
|
||||
},
|
||||
} as any)
|
||||
).rejects.toThrow(error);
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
{ error, userId: ctx.user.id, projectId: baseProject.id },
|
||||
"Workspace deletion failed"
|
||||
);
|
||||
});
|
||||
|
||||
test("does not error-log expected deletion failures", async () => {
|
||||
const error = new InvalidInputError("Workspace name confirmation does not match");
|
||||
mocks.deleteProjectWithConfirmation.mockRejectedValueOnce(error);
|
||||
|
||||
await expect(
|
||||
deleteProjectAction({
|
||||
ctx,
|
||||
parsedInput: {
|
||||
projectId: baseProject.id,
|
||||
confirmationName: "Other Workspace",
|
||||
},
|
||||
} as any)
|
||||
).rejects.toThrow(error);
|
||||
|
||||
expect(logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,44 +1,35 @@
|
||||
"use server";
|
||||
|
||||
import { z } from "zod";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { getProject, getUserProjects } from "@/lib/project/service";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { isExpectedError } from "@formbricks/types/errors";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
import { getOrganizationIdFromProjectId } from "@/lib/utils/helper";
|
||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||
import { deleteProject } from "@/modules/projects/settings/lib/project";
|
||||
import { deleteProjectWithConfirmation, getProjectIdForLogging } from "./lib/delete-project";
|
||||
|
||||
const ZProjectDeleteAction = z.object({
|
||||
projectId: ZId,
|
||||
});
|
||||
const logProjectDeletionError = (userId: string, projectId: string, error: unknown) => {
|
||||
logger.error({ error, userId, projectId }, "Workspace deletion failed");
|
||||
};
|
||||
|
||||
export const deleteProjectAction = authenticatedActionClient.inputSchema(ZProjectDeleteAction).action(
|
||||
const shouldLogProjectDeletionError = (error: unknown) => {
|
||||
return !(error instanceof Error && isExpectedError(error));
|
||||
};
|
||||
|
||||
export const deleteProjectAction = authenticatedActionClient.inputSchema(z.unknown()).action(
|
||||
withAuditLogging("deleted", "project", async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromProjectId(parsedInput.projectId);
|
||||
const projectIdForLogging = getProjectIdForLogging(parsedInput);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const availableProjects = (await getUserProjects(ctx.user.id, organizationId)) ?? null;
|
||||
|
||||
if (!!availableProjects && availableProjects?.length <= 1) {
|
||||
throw new Error("You can't delete the last project in the environment.");
|
||||
try {
|
||||
return await deleteProjectWithConfirmation({
|
||||
input: parsedInput,
|
||||
userId: ctx.user.id,
|
||||
auditLoggingCtx: ctx.auditLoggingCtx,
|
||||
});
|
||||
} catch (error) {
|
||||
if (shouldLogProjectDeletionError(error)) {
|
||||
logProjectDeletionError(ctx.user.id, projectIdForLogging, error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.projectId = parsedInput.projectId;
|
||||
ctx.auditLoggingCtx.oldObject = await getProject(parsedInput.projectId);
|
||||
|
||||
// delete project
|
||||
return await deleteProject(parsedInput.projectId);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -4,14 +4,17 @@ import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { truncate } from "@/lib/utils/strings";
|
||||
import { deleteProjectAction } from "@/modules/projects/settings/general/actions";
|
||||
import { hasMatchingWorkspaceDeleteConfirmation } from "@/modules/projects/settings/general/lib/delete-project-confirmation";
|
||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
|
||||
interface DeleteProjectRenderProps {
|
||||
isDeleteDisabled: boolean;
|
||||
@@ -30,30 +33,55 @@ export const DeleteProjectRender = ({
|
||||
const router = useRouter();
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const handleDeleteProject = async () => {
|
||||
setIsDeleting(true);
|
||||
const deleteProjectResponse = await deleteProjectAction({ projectId: currentProject.id });
|
||||
if (deleteProjectResponse?.data) {
|
||||
if (organizationProjects.length === 1) {
|
||||
localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS);
|
||||
} else if (organizationProjects.length > 1) {
|
||||
// prevents changing of organization when deleting project
|
||||
const remainingProjects = organizationProjects.filter((project) => project.id !== currentProject.id);
|
||||
const productionEnvironment = remainingProjects[0].environments.find(
|
||||
(environment) => environment.type === "production"
|
||||
);
|
||||
if (productionEnvironment) {
|
||||
localStorage.setItem(FORMBRICKS_ENVIRONMENT_ID_LS, productionEnvironment.id);
|
||||
}
|
||||
}
|
||||
toast.success(t("environments.workspace.general.workspace_deleted_successfully"));
|
||||
router.push("/");
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(deleteProjectResponse);
|
||||
toast.error(errorMessage);
|
||||
setIsDeleteDialogOpen(false);
|
||||
const [confirmationName, setConfirmationName] = useState("");
|
||||
const hasValidConfirmation = hasMatchingWorkspaceDeleteConfirmation(confirmationName, currentProject.name);
|
||||
|
||||
const handleDeleteDialogOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
setConfirmationName("");
|
||||
}
|
||||
setIsDeleteDialogOpen(open);
|
||||
};
|
||||
|
||||
const handleDeleteProject = async () => {
|
||||
if (!hasValidConfirmation) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
const deleteProjectResponse = await deleteProjectAction({
|
||||
projectId: currentProject.id,
|
||||
confirmationName,
|
||||
});
|
||||
|
||||
if (deleteProjectResponse?.data) {
|
||||
if (organizationProjects.length === 1) {
|
||||
localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS);
|
||||
} else if (organizationProjects.length > 1) {
|
||||
// prevents changing of organization when deleting project
|
||||
const remainingProject = organizationProjects.find((project) => project.id !== currentProject.id);
|
||||
const productionEnvironment = remainingProject?.environments.find(
|
||||
(environment) => environment.type === "production"
|
||||
);
|
||||
if (productionEnvironment) {
|
||||
localStorage.setItem(FORMBRICKS_ENVIRONMENT_ID_LS, productionEnvironment.id);
|
||||
}
|
||||
}
|
||||
toast.success(t("environments.workspace.general.workspace_deleted_successfully"));
|
||||
router.push("/");
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(deleteProjectResponse);
|
||||
logger.error({ errorMessage, projectId: currentProject.id }, "Workspace deletion action failed");
|
||||
toast.error(errorMessage);
|
||||
handleDeleteDialogOpenChange(false);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error({ error, projectId: currentProject.id }, "Workspace deletion failed");
|
||||
toast.error(t("common.something_went_wrong_please_try_again"));
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
setIsDeleting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -91,13 +119,36 @@ export const DeleteProjectRender = ({
|
||||
<DeleteDialog
|
||||
deleteWhat={t("environments.settings.domain.workspace")}
|
||||
open={isDeleteDialogOpen}
|
||||
setOpen={setIsDeleteDialogOpen}
|
||||
setOpen={handleDeleteDialogOpenChange}
|
||||
onDelete={handleDeleteProject}
|
||||
text={t("environments.workspace.general.delete_workspace_confirmation", {
|
||||
projectName: truncate(currentProject.name, 30),
|
||||
})}
|
||||
isDeleting={isDeleting}
|
||||
/>
|
||||
disabled={!hasValidConfirmation}>
|
||||
<div className="py-5">
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
await handleDeleteProject();
|
||||
}}>
|
||||
<label htmlFor="deleteProjectConfirmation">
|
||||
{t("environments.workspace.general.delete_workspace_confirmation_name", {
|
||||
projectName: currentProject.name,
|
||||
})}
|
||||
</label>
|
||||
<Input
|
||||
value={confirmationName}
|
||||
onChange={(e) => setConfirmationName(e.target.value)}
|
||||
placeholder={currentProject.name}
|
||||
className="mt-2"
|
||||
type="text"
|
||||
id="deleteProjectConfirmation"
|
||||
name="deleteProjectConfirmation"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</DeleteDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { hasMatchingWorkspaceDeleteConfirmation } from "./delete-project-confirmation";
|
||||
|
||||
describe("workspace delete confirmation", () => {
|
||||
test("accepts an exact workspace name match", () => {
|
||||
expect(hasMatchingWorkspaceDeleteConfirmation("Acme Workspace", "Acme Workspace")).toBe(true);
|
||||
});
|
||||
|
||||
test("accepts different casing", () => {
|
||||
expect(hasMatchingWorkspaceDeleteConfirmation("acme workspace", "Acme Workspace")).toBe(true);
|
||||
});
|
||||
|
||||
test("accepts leading and trailing whitespace", () => {
|
||||
expect(hasMatchingWorkspaceDeleteConfirmation(" Acme Workspace ", "Acme Workspace")).toBe(true);
|
||||
});
|
||||
|
||||
test("rejects an empty confirmation", () => {
|
||||
expect(hasMatchingWorkspaceDeleteConfirmation("", "Acme Workspace")).toBe(false);
|
||||
});
|
||||
|
||||
test("rejects mismatched confirmations", () => {
|
||||
expect(hasMatchingWorkspaceDeleteConfirmation("Other Workspace", "Acme Workspace")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
export const WORKSPACE_DELETE_CONFIRMATION_ERROR = "Workspace name confirmation does not match";
|
||||
|
||||
const normalizeWorkspaceNameConfirmation = (value: string) => value.trim().toLowerCase();
|
||||
|
||||
export const hasMatchingWorkspaceDeleteConfirmation = (
|
||||
confirmationName: string,
|
||||
workspaceName: string
|
||||
): boolean => {
|
||||
return (
|
||||
normalizeWorkspaceNameConfirmation(confirmationName) === normalizeWorkspaceNameConfirmation(workspaceName)
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,170 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import {
|
||||
AuthorizationError,
|
||||
InvalidInputError,
|
||||
OperationNotAllowedError,
|
||||
ResourceNotFoundError,
|
||||
} from "@formbricks/types/errors";
|
||||
import {
|
||||
DELETE_PROJECT_CONFIRMATION_REQUIRED_ERROR,
|
||||
deleteProjectWithConfirmation,
|
||||
getProjectIdForLogging,
|
||||
} from "./delete-project";
|
||||
import { WORKSPACE_DELETE_CONFIRMATION_ERROR } from "./delete-project-confirmation";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
checkAuthorizationUpdated: vi.fn(),
|
||||
deleteProject: vi.fn(),
|
||||
getProject: vi.fn(),
|
||||
getUserProjects: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/project/service", () => ({
|
||||
getProject: mocks.getProject,
|
||||
getUserProjects: mocks.getUserProjects,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/action-client/action-client-middleware", () => ({
|
||||
checkAuthorizationUpdated: mocks.checkAuthorizationUpdated,
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/projects/settings/lib/project", () => ({
|
||||
deleteProject: mocks.deleteProject,
|
||||
}));
|
||||
|
||||
const baseProject = {
|
||||
id: "cmproject000000000000000000",
|
||||
name: "Acme Workspace",
|
||||
organizationId: "cmorg00000000000000000000",
|
||||
};
|
||||
|
||||
const userId = "cmuser00000000000000000000";
|
||||
|
||||
const callDeleteProjectWithConfirmation = (input = {}) =>
|
||||
deleteProjectWithConfirmation({
|
||||
input: {
|
||||
projectId: baseProject.id,
|
||||
confirmationName: baseProject.name,
|
||||
...input,
|
||||
},
|
||||
userId,
|
||||
auditLoggingCtx: {},
|
||||
});
|
||||
|
||||
describe("deleteProjectWithConfirmation", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.checkAuthorizationUpdated.mockResolvedValue(undefined);
|
||||
mocks.getProject.mockResolvedValue(baseProject);
|
||||
mocks.getUserProjects.mockResolvedValue([baseProject, { ...baseProject, id: "cmproject2" }]);
|
||||
mocks.deleteProject.mockResolvedValue(baseProject);
|
||||
});
|
||||
|
||||
test("deletes a workspace when the confirmation name matches", async () => {
|
||||
const auditLoggingCtx = {};
|
||||
|
||||
const result = await deleteProjectWithConfirmation({
|
||||
input: {
|
||||
projectId: baseProject.id,
|
||||
confirmationName: "acme workspace",
|
||||
},
|
||||
userId,
|
||||
auditLoggingCtx,
|
||||
});
|
||||
|
||||
expect(mocks.checkAuthorizationUpdated).toHaveBeenCalledWith({
|
||||
userId,
|
||||
organizationId: baseProject.organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(mocks.getUserProjects).toHaveBeenCalledWith(userId, baseProject.organizationId);
|
||||
expect(mocks.deleteProject).toHaveBeenCalledWith(baseProject.id);
|
||||
expect(auditLoggingCtx).toMatchObject({
|
||||
organizationId: baseProject.organizationId,
|
||||
projectId: baseProject.id,
|
||||
oldObject: baseProject,
|
||||
});
|
||||
expect(result).toEqual(baseProject);
|
||||
});
|
||||
|
||||
test("rejects invalid input before any project lookup", async () => {
|
||||
await expect(
|
||||
deleteProjectWithConfirmation({
|
||||
input: {},
|
||||
userId,
|
||||
auditLoggingCtx: {},
|
||||
})
|
||||
).rejects.toThrow(InvalidInputError);
|
||||
await expect(
|
||||
deleteProjectWithConfirmation({
|
||||
input: {},
|
||||
userId,
|
||||
auditLoggingCtx: {},
|
||||
})
|
||||
).rejects.toThrow(DELETE_PROJECT_CONFIRMATION_REQUIRED_ERROR);
|
||||
|
||||
expect(mocks.getProject).not.toHaveBeenCalled();
|
||||
expect(mocks.deleteProject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does not delete when the confirmation name does not match", async () => {
|
||||
await expect(callDeleteProjectWithConfirmation({ confirmationName: "Other Workspace" })).rejects.toThrow(
|
||||
InvalidInputError
|
||||
);
|
||||
await expect(callDeleteProjectWithConfirmation({ confirmationName: "Other Workspace" })).rejects.toThrow(
|
||||
WORKSPACE_DELETE_CONFIRMATION_ERROR
|
||||
);
|
||||
|
||||
expect(mocks.deleteProject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does not delete when the workspace cannot be found", async () => {
|
||||
mocks.getProject.mockResolvedValueOnce(null);
|
||||
|
||||
await expect(callDeleteProjectWithConfirmation()).rejects.toThrow(ResourceNotFoundError);
|
||||
|
||||
expect(mocks.checkAuthorizationUpdated).not.toHaveBeenCalled();
|
||||
expect(mocks.deleteProject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does not delete when authorization fails", async () => {
|
||||
mocks.checkAuthorizationUpdated.mockRejectedValueOnce(new AuthorizationError("Not authorized"));
|
||||
|
||||
await expect(callDeleteProjectWithConfirmation()).rejects.toThrow(AuthorizationError);
|
||||
|
||||
expect(mocks.getUserProjects).not.toHaveBeenCalled();
|
||||
expect(mocks.deleteProject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does not delete the last available workspace", async () => {
|
||||
mocks.getUserProjects.mockResolvedValueOnce([baseProject]);
|
||||
|
||||
await expect(callDeleteProjectWithConfirmation()).rejects.toThrow(OperationNotAllowedError);
|
||||
|
||||
expect(mocks.deleteProject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("rethrows downstream delete failures", async () => {
|
||||
const error = new Error("delete failed");
|
||||
mocks.deleteProject.mockRejectedValueOnce(error);
|
||||
|
||||
await expect(callDeleteProjectWithConfirmation()).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getProjectIdForLogging", () => {
|
||||
test("returns the project id when present", () => {
|
||||
expect(getProjectIdForLogging({ projectId: baseProject.id })).toBe(baseProject.id);
|
||||
});
|
||||
|
||||
test("returns unknown when the project id is missing or invalid", () => {
|
||||
expect(getProjectIdForLogging({})).toBe("unknown");
|
||||
expect(getProjectIdForLogging({ projectId: 123 })).toBe("unknown");
|
||||
expect(getProjectIdForLogging(null)).toBe("unknown");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { z } from "zod";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { InvalidInputError, OperationNotAllowedError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { getProject, getUserProjects } from "@/lib/project/service";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
import { deleteProject } from "@/modules/projects/settings/lib/project";
|
||||
import {
|
||||
WORKSPACE_DELETE_CONFIRMATION_ERROR,
|
||||
hasMatchingWorkspaceDeleteConfirmation,
|
||||
} from "./delete-project-confirmation";
|
||||
|
||||
const ZProjectDeleteAction = z.object({
|
||||
projectId: ZId,
|
||||
confirmationName: z.string().trim().min(1),
|
||||
});
|
||||
|
||||
export const DELETE_PROJECT_CONFIRMATION_REQUIRED_ERROR =
|
||||
"Workspace name confirmation is required to delete this workspace.";
|
||||
|
||||
export const parseProjectDeleteActionInput = (input: unknown) => {
|
||||
const parsedInput = ZProjectDeleteAction.safeParse(input);
|
||||
|
||||
if (!parsedInput.success) {
|
||||
throw new InvalidInputError(DELETE_PROJECT_CONFIRMATION_REQUIRED_ERROR);
|
||||
}
|
||||
|
||||
return parsedInput.data;
|
||||
};
|
||||
|
||||
export const getProjectIdForLogging = (input: unknown) => {
|
||||
if (typeof input !== "object" || input === null || !("projectId" in input)) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const projectId = input.projectId;
|
||||
|
||||
return typeof projectId === "string" ? projectId : "unknown";
|
||||
};
|
||||
|
||||
const assertMatchingWorkspaceDeleteConfirmation = (confirmationName: string, workspaceName: string) => {
|
||||
if (!hasMatchingWorkspaceDeleteConfirmation(confirmationName, workspaceName)) {
|
||||
throw new InvalidInputError(WORKSPACE_DELETE_CONFIRMATION_ERROR);
|
||||
}
|
||||
};
|
||||
|
||||
interface DeleteProjectWithConfirmationParams {
|
||||
input: unknown;
|
||||
userId: string;
|
||||
auditLoggingCtx: {
|
||||
organizationId?: string;
|
||||
projectId?: string;
|
||||
oldObject?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export const deleteProjectWithConfirmation = async ({
|
||||
input,
|
||||
userId,
|
||||
auditLoggingCtx,
|
||||
}: DeleteProjectWithConfirmationParams) => {
|
||||
const { confirmationName, projectId } = parseProjectDeleteActionInput(input);
|
||||
const project = await getProject(projectId);
|
||||
|
||||
if (!project) {
|
||||
throw new ResourceNotFoundError("project", projectId);
|
||||
}
|
||||
|
||||
const organizationId = project.organizationId;
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const availableProjects = await getUserProjects(userId, organizationId);
|
||||
|
||||
if (availableProjects.length <= 1) {
|
||||
throw new OperationNotAllowedError("You can't delete the last project in the environment.");
|
||||
}
|
||||
|
||||
assertMatchingWorkspaceDeleteConfirmation(confirmationName, project.name);
|
||||
|
||||
auditLoggingCtx.organizationId = organizationId;
|
||||
auditLoggingCtx.projectId = projectId;
|
||||
auditLoggingCtx.oldObject = project;
|
||||
|
||||
return await deleteProject(projectId);
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
const createEnvironmentData = (type: "development" | "production") => ({
|
||||
type,
|
||||
attributeKeys: {
|
||||
create: [
|
||||
{
|
||||
name: "Email",
|
||||
key: "email",
|
||||
isUnique: true,
|
||||
type: "default" as const,
|
||||
},
|
||||
{
|
||||
name: "First Name",
|
||||
key: "firstName",
|
||||
isUnique: false,
|
||||
type: "default" as const,
|
||||
},
|
||||
{
|
||||
name: "Last Name",
|
||||
key: "lastName",
|
||||
isUnique: false,
|
||||
type: "default" as const,
|
||||
},
|
||||
{
|
||||
name: "userId",
|
||||
key: "userId",
|
||||
isUnique: true,
|
||||
type: "default" as const,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const getProjectForEmail = async (email: string) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
select: {
|
||||
memberships: {
|
||||
select: {
|
||||
organizationId: true,
|
||||
organization: {
|
||||
select: {
|
||||
projects: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
environments: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const membership = user?.memberships[0];
|
||||
const project = membership?.organization.projects[0];
|
||||
const productionEnvironment = project?.environments.find(
|
||||
(environment) => environment.type === "production"
|
||||
);
|
||||
|
||||
if (!membership || !project || !productionEnvironment) {
|
||||
throw new Error(`Project not found for email: ${email}`);
|
||||
}
|
||||
|
||||
return {
|
||||
organizationId: membership.organizationId,
|
||||
projectId: project.id,
|
||||
projectName: project.name,
|
||||
productionEnvironmentId: productionEnvironment.id,
|
||||
};
|
||||
};
|
||||
|
||||
test("requires project name confirmation before deleting a project", async ({ page, users }) => {
|
||||
const timestamp = Date.now();
|
||||
const email = `project-delete-${timestamp}@example.com`;
|
||||
const projectName = `Delete Project ${timestamp}`;
|
||||
const remainingProjectName = `Remaining Project ${timestamp}`;
|
||||
const user = await users.create({
|
||||
email,
|
||||
name: `project-delete-${timestamp}`,
|
||||
projectName,
|
||||
});
|
||||
const project = await getProjectForEmail(email);
|
||||
const remainingProject = await prisma.project.create({
|
||||
data: {
|
||||
name: remainingProjectName,
|
||||
organizationId: project.organizationId,
|
||||
environments: {
|
||||
create: [createEnvironmentData("development"), createEnvironmentData("production")],
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
const remainingProductionEnvironment = await prisma.environment.findFirst({
|
||||
where: {
|
||||
projectId: remainingProject.id,
|
||||
type: "production",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
const remainingProductionEnvironmentId = remainingProductionEnvironment?.id;
|
||||
|
||||
if (!remainingProductionEnvironmentId) {
|
||||
throw new Error("Remaining project production environment not found");
|
||||
}
|
||||
|
||||
await user.login();
|
||||
await page.goto(`/environments/${project.productionEnvironmentId}/workspace/general`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
|
||||
await page.getByRole("button", { name: "Delete", exact: true }).click();
|
||||
const dialog = page.getByRole("dialog");
|
||||
await expect(dialog.getByRole("button", { name: "Delete", exact: true })).toBeDisabled();
|
||||
|
||||
await page.locator("#deleteProjectConfirmation").fill(project.projectName.toUpperCase());
|
||||
await expect(dialog.getByRole("button", { name: "Delete", exact: true })).toBeEnabled();
|
||||
await dialog.getByRole("button", { name: "Delete", exact: true }).click();
|
||||
|
||||
await expect(page.getByText("Workspace deleted successfully", { exact: true })).toBeVisible();
|
||||
await page.waitForURL(new RegExp(`/environments/${remainingProductionEnvironmentId}/surveys`));
|
||||
await expect.poll(async () => prisma.project.findUnique({ where: { id: project.projectId } })).toBeNull();
|
||||
});
|
||||
Reference in New Issue
Block a user