From ba9b01a9699d56a54bc2fb9670953bbd1c0d5a50 Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Fri, 4 Jul 2025 13:46:27 +0530 Subject: [PATCH] fix: survey list refresh (#6104) Co-authored-by: Victor Santos --- apps/web/locales/de-DE.json | 23 +- apps/web/locales/en-US.json | 23 +- apps/web/locales/fr-FR.json | 23 +- apps/web/locales/pt-BR.json | 23 +- apps/web/locales/pt-PT.json | 23 +- apps/web/locales/zh-Hant-TW.json | 23 +- .../list/components/copy-survey-form.test.tsx | 367 +++++++++++++++--- .../list/components/copy-survey-form.tsx | 222 +++++++---- .../survey/list/components/survey-card.tsx | 6 +- .../components/survey-dropdown-menu.test.tsx | 22 -- .../list/components/survey-dropdown-menu.tsx | 9 +- .../list/components/survey-list.test.tsx | 21 - .../survey/list/components/survey-list.tsx | 12 +- 13 files changed, 486 insertions(+), 311 deletions(-) diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json index 0c2794a353..d9503d1489 100644 --- a/apps/web/locales/de-DE.json +++ b/apps/web/locales/de-DE.json @@ -980,43 +980,29 @@ "api_keys_description": "Verwalte API-Schlüssel, um auf die Formbricks-Management-APIs zuzugreifen" }, "billing": { - "10000_monthly_responses": "10,000 monatliche Antworten", "1000_monthly_responses": "1,000 monatliche Antworten", - "1500_monthly_responses": "1,500 monatliche Antworten", "1_project": "1 Projekt", "2000_contacts": "2,000 Kontakte", - "2000_monthly_identified_users": "2,000 monatlich identifizierte Nutzer", - "30000_monthly_identified_users": "30,000 monatlich identifizierte Nutzer", "3_projects": "3 Projekte", "5000_monthly_responses": "5,000 monatliche Antworten", - "5_projects": "5 Projekte", "7500_contacts": "7,500 Kontakte", - "7500_monthly_identified_users": "7,500 monatlich identifizierte Nutzer", - "advanced_targeting": "Erweitertes Targeting", "all_integrations": "Alle Integrationen", - "all_surveying_features": "Alle Umfragefunktionen", "annually": "Jährlich", "api_webhooks": "API & Webhooks", "app_surveys": "In-app Umfragen", "attribute_based_targeting": "Attributbasiertes Targeting", - "contact_us": "Kontaktiere uns", "current": "aktuell", "current_plan": "Aktueller Plan", "current_tier_limit": "Aktuelles Limit", "custom": "Benutzerdefiniert & Skalierung", "custom_contacts_limit": "Benutzerdefiniertes Kontaktlimit", - "custom_miu_limit": "Benutzerdefiniertes MIU-Limit", "custom_project_limit": "Benutzerdefiniertes Projektlimit", "custom_response_limit": "Benutzerdefiniertes Antwortlimit", - "customer_success_manager": "Customer Success Manager", "email_embedded_surveys": "Eingebettete Umfragen in E-Mails", "email_follow_ups": "E-Mail Follow-ups", - "email_support": "E-Mail-Support", - "enterprise": "Enterprise", "enterprise_description": "Premium-Support und benutzerdefinierte Limits.", "everybody_has_the_free_plan_by_default": "Jeder hat standardmäßig den kostenlosen Plan!", "everything_in_free": "Alles in 'Free''", - "everything_in_scale": "Alles in 'Scale''", "everything_in_startup": "Alles in 'Startup''", "free": "Kostenlos", "free_description": "Unbegrenzte Umfragen, Teammitglieder und mehr.", @@ -1030,25 +1016,17 @@ "manage_subscription": "Abonnement verwalten", "monthly": "Monatlich", "monthly_identified_users": "Monatlich identifizierte Nutzer", - "multi_language_surveys": "Mehrsprachige Umfragen", "per_month": "pro Monat", "per_year": "pro Jahr", "plan_upgraded_successfully": "Plan erfolgreich aktualisiert", "premium_support_with_slas": "Premium-Support mit SLAs", - "priority_support": "Priorisierter Support", "remove_branding": "Branding entfernen", - "say_hi": "Sag Hi!", - "scale": "Scale", - "scale_and_enterprise": "Scale & Enterprise", - "scale_description": "Erweiterte Funktionen für größere Unternehmen.", "startup": "Start-up", "startup_description": "Alles in 'Free' mit zusätzlichen Funktionen.", "switch_plan": "Plan wechseln", "switch_plan_confirmation_text": "Bist du sicher, dass du zum {plan}-Plan wechseln möchtest? Dir werden {price} {period} berechnet.", "team_access_roles": "Rollen für Teammitglieder", - "technical_onboarding": "Technische Einführung", "unable_to_upgrade_plan": "Plan kann nicht aktualisiert werden", - "unlimited_apps_websites": "Unbegrenzte Apps & Websites", "unlimited_miu": "Unbegrenzte MIU", "unlimited_projects": "Unbegrenzte Projekte", "unlimited_responses": "Unbegrenzte Antworten", @@ -1243,6 +1221,7 @@ "copy_survey_description": "Kopiere diese Umfrage in eine andere Umgebung", "copy_survey_error": "Kopieren der Umfrage fehlgeschlagen", "copy_survey_link_to_clipboard": "Umfragelink in die Zwischenablage kopieren", + "copy_survey_partially_success": "{success} Umfragen erfolgreich kopiert, {error} fehlgeschlagen.", "copy_survey_success": "Umfrage erfolgreich kopiert!", "delete_survey_and_responses_warning": "Bist Du sicher, dass Du diese Umfrage und alle ihre Antworten löschen möchtest?", "edit": { diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json index 07cc84d7ba..67a2292633 100644 --- a/apps/web/locales/en-US.json +++ b/apps/web/locales/en-US.json @@ -980,43 +980,29 @@ "api_keys_description": "Manage API keys to access Formbricks management APIs" }, "billing": { - "10000_monthly_responses": "10000 Monthly Responses", "1000_monthly_responses": "Monthly 1,000 Responses", - "1500_monthly_responses": "1500 Monthly Responses", "1_project": "1 Project", "2000_contacts": "2,000 Contacts", - "2000_monthly_identified_users": "2000 Monthly Identified Users", - "30000_monthly_identified_users": "30000 Monthly Identified Users", "3_projects": "3 Projects", "5000_monthly_responses": "5,000 Monthly Responses", - "5_projects": "5 Projects", "7500_contacts": "7,500 Contacts", - "7500_monthly_identified_users": "7500 Monthly Identified Users", - "advanced_targeting": "Advanced Targeting", "all_integrations": "All Integrations", - "all_surveying_features": "All surveying features", "annually": "Annually", "api_webhooks": "API & Webhooks", "app_surveys": "App Surveys", "attribute_based_targeting": "Attribute-based Targeting", - "contact_us": "Contact Us", "current": "Current", "current_plan": "Current Plan", "current_tier_limit": "Current Tier Limit", "custom": "Custom & Scale", "custom_contacts_limit": "Custom Contacts Limit", - "custom_miu_limit": "Custom MIU limit", "custom_project_limit": "Custom Project Limit", "custom_response_limit": "Custom Response Limit", - "customer_success_manager": "Customer Success Manager", "email_embedded_surveys": "Email Embedded Surveys", "email_follow_ups": "Email Follow-ups", - "email_support": "Email Support", - "enterprise": "Enterprise", "enterprise_description": "Premium support and custom limits.", "everybody_has_the_free_plan_by_default": "Everybody has the free plan by default!", "everything_in_free": "Everything in Free", - "everything_in_scale": "Everything in Scale", "everything_in_startup": "Everything in Startup", "free": "Free", "free_description": "Unlimited Surveys, Team Members, and more.", @@ -1030,25 +1016,17 @@ "manage_subscription": "Manage Subscription", "monthly": "Monthly", "monthly_identified_users": "Monthly Identified Users", - "multi_language_surveys": "Multi-Language Surveys", "per_month": "per month", "per_year": "per year", "plan_upgraded_successfully": "Plan upgraded successfully", "premium_support_with_slas": "Premium support with SLAs", - "priority_support": "Priority Support", "remove_branding": "Remove Branding", - "say_hi": "Say Hi!", - "scale": "Scale", - "scale_and_enterprise": "Scale & Enterprise", - "scale_description": "Advanced features for scaling your business.", "startup": "Startup", "startup_description": "Everything in Free with additional features.", "switch_plan": "Switch Plan", "switch_plan_confirmation_text": "Are you sure you want to switch to the {plan} plan? You will be charged {price} {period}.", "team_access_roles": "Team Access Roles", - "technical_onboarding": "Technical Onboarding", "unable_to_upgrade_plan": "Unable to upgrade plan", - "unlimited_apps_websites": "Unlimited Apps & Websites", "unlimited_miu": "Unlimited MIU", "unlimited_projects": "Unlimited Projects", "unlimited_responses": "Unlimited Responses", @@ -1243,6 +1221,7 @@ "copy_survey_description": "Copy this survey to another environment", "copy_survey_error": "Failed to copy survey", "copy_survey_link_to_clipboard": "Copy survey link to clipboard", + "copy_survey_partially_success": "{success} surveys copied successfully, {error} failed.", "copy_survey_success": "Survey copied successfully!", "delete_survey_and_responses_warning": "Are you sure you want to delete this survey and all of its responses?", "edit": { diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json index d8e8b4d6a2..312cb4d472 100644 --- a/apps/web/locales/fr-FR.json +++ b/apps/web/locales/fr-FR.json @@ -980,43 +980,29 @@ "api_keys_description": "Gérer les clés API pour accéder aux API de gestion de Formbricks" }, "billing": { - "10000_monthly_responses": "10000 Réponses Mensuelles", "1000_monthly_responses": "1000 Réponses Mensuelles", - "1500_monthly_responses": "1500 Réponses Mensuelles", "1_project": "1 Projet", "2000_contacts": "2 000 Contacts", - "2000_monthly_identified_users": "2000 Utilisateurs Identifiés Mensuels", - "30000_monthly_identified_users": "30000 Utilisateurs Identifiés Mensuels", "3_projects": "3 Projets", "5000_monthly_responses": "5,000 Réponses Mensuelles", - "5_projects": "5 Projets", "7500_contacts": "7 500 Contacts", - "7500_monthly_identified_users": "7500 Utilisateurs Identifiés Mensuels", - "advanced_targeting": "Ciblage Avancé", "all_integrations": "Toutes les intégrations", - "all_surveying_features": "Tous les outils d'arpentage", "annually": "Annuellement", "api_webhooks": "API et Webhooks", "app_surveys": "Sondages d'application", "attribute_based_targeting": "Ciblage basé sur les attributs", - "contact_us": "Contactez-nous", "current": "Actuel", "current_plan": "Plan actuel", "current_tier_limit": "Limite de niveau actuel", "custom": "Personnalisé et Échelle", "custom_contacts_limit": "Limite de contacts personnalisé", - "custom_miu_limit": "Limite MIU personnalisé", "custom_project_limit": "Limite de projet personnalisé", "custom_response_limit": "Limite de réponse personnalisé", - "customer_success_manager": "Responsable de la réussite client", "email_embedded_surveys": "Sondages intégrés par e-mail", "email_follow_ups": "Relances par e-mail", - "email_support": "Support par e-mail", - "enterprise": "Entreprise", "enterprise_description": "Soutien premium et limites personnalisées.", "everybody_has_the_free_plan_by_default": "Tout le monde a le plan gratuit par défaut !", "everything_in_free": "Tout est gratuit", - "everything_in_scale": "Tout à l'échelle", "everything_in_startup": "Tout dans le Startup", "free": "Gratuit", "free_description": "Sondages illimités, membres d'équipe, et plus encore.", @@ -1030,25 +1016,17 @@ "manage_subscription": "Gérer l'abonnement", "monthly": "Mensuel", "monthly_identified_users": "Utilisateurs Identifiés Mensuels", - "multi_language_surveys": "Sondages multilingues", "per_month": "par mois", "per_year": "par an", "plan_upgraded_successfully": "Plan mis à jour avec succès", "premium_support_with_slas": "Soutien premium avec SLA", - "priority_support": "Soutien Prioritaire", "remove_branding": "Supprimer la marque", - "say_hi": "Dis bonjour !", - "scale": "Échelle", - "scale_and_enterprise": "Échelle et Entreprise", - "scale_description": "Fonctionnalités avancées pour développer votre entreprise.", "startup": "Startup", "startup_description": "Tout est gratuit avec des fonctionnalités supplémentaires.", "switch_plan": "Changer de plan", "switch_plan_confirmation_text": "Êtes-vous sûr de vouloir passer au plan {plan} ? Vous serez facturé {price} {period}.", "team_access_roles": "Rôles d'accès d'équipe", - "technical_onboarding": "Intégration technique", "unable_to_upgrade_plan": "Impossible de mettre à niveau le plan", - "unlimited_apps_websites": "Applications et sites Web illimités", "unlimited_miu": "MIU Illimité", "unlimited_projects": "Projets illimités", "unlimited_responses": "Réponses illimitées", @@ -1243,6 +1221,7 @@ "copy_survey_description": "Copier cette enquête dans un autre environnement", "copy_survey_error": "Échec de la copie du sondage", "copy_survey_link_to_clipboard": "Copier le lien du sondage dans le presse-papiers", + "copy_survey_partially_success": "{success} enquêtes copiées avec succès, {error} échouées.", "copy_survey_success": "Enquête copiée avec succès !", "delete_survey_and_responses_warning": "Êtes-vous sûr de vouloir supprimer cette enquête et toutes ses réponses?", "edit": { diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json index 1fd900890b..e2311da469 100644 --- a/apps/web/locales/pt-BR.json +++ b/apps/web/locales/pt-BR.json @@ -980,43 +980,29 @@ "api_keys_description": "Gerencie chaves de API para acessar as APIs de gerenciamento do Formbricks" }, "billing": { - "10000_monthly_responses": "10000 Respostas Mensais", "1000_monthly_responses": "1000 Respostas Mensais", - "1500_monthly_responses": "1500 Respostas Mensais", "1_project": "1 Projeto", "2000_contacts": "2.000 Contatos", - "2000_monthly_identified_users": "2000 Usuários Identificados Mensalmente", - "30000_monthly_identified_users": "30000 Usuários Identificados Mensalmente", "3_projects": "3 Projetos", "5000_monthly_responses": "5,000 Respostas Mensais", - "5_projects": "5 Projetos", "7500_contacts": "7.500 Contatos", - "7500_monthly_identified_users": "7500 Usuários Identificados Mensalmente", - "advanced_targeting": "Mira Avançada", "all_integrations": "Todas as Integrações", - "all_surveying_features": "Todos os recursos de levantamento", "annually": "anualmente", "api_webhooks": "API e Webhooks", "app_surveys": "Pesquisas de App", "attribute_based_targeting": "Segmentação Baseada em Atributos", - "contact_us": "Fale Conosco", "current": "atual", "current_plan": "Plano Atual", "current_tier_limit": "Limite Atual de Nível", "custom": "Personalizado e Escala", "custom_contacts_limit": "Limite de Contatos Personalizado", - "custom_miu_limit": "Limite MIU personalizado", "custom_project_limit": "Limite de Projeto Personalizado", "custom_response_limit": "Limite de Resposta Personalizado", - "customer_success_manager": "Gerente de Sucesso do Cliente", "email_embedded_surveys": "Pesquisas Incorporadas no Email", "email_follow_ups": "Acompanhamentos por Email", - "email_support": "Suporte por Email", - "enterprise": "Empresa", "enterprise_description": "Suporte premium e limites personalizados.", "everybody_has_the_free_plan_by_default": "Todo mundo tem o plano gratuito por padrão!", "everything_in_free": "Tudo de graça", - "everything_in_scale": "Tudo em Escala", "everything_in_startup": "Tudo em Startup", "free": "grátis", "free_description": "Pesquisas ilimitadas, membros da equipe e mais.", @@ -1030,25 +1016,17 @@ "manage_subscription": "Gerenciar Assinatura", "monthly": "mensal", "monthly_identified_users": "Usuários Identificados Mensalmente", - "multi_language_surveys": "Pesquisas Multilíngues", "per_month": "por mês", "per_year": "por ano", "plan_upgraded_successfully": "Plano atualizado com sucesso", "premium_support_with_slas": "Suporte premium com SLAs", - "priority_support": "Suporte Prioritário", "remove_branding": "Remover Marca", - "say_hi": "Diz oi!", - "scale": "escala", - "scale_and_enterprise": "Escala e Empresa", - "scale_description": "Recursos avançados pra escalar seu negócio.", "startup": "startup", "startup_description": "Tudo no Grátis com recursos adicionais.", "switch_plan": "Mudar Plano", "switch_plan_confirmation_text": "Tem certeza de que deseja mudar para o plano {plan}? Você será cobrado {price} {period}.", "team_access_roles": "Funções de Acesso da Equipe", - "technical_onboarding": "Integração Técnica", "unable_to_upgrade_plan": "Não foi possível atualizar o plano", - "unlimited_apps_websites": "Apps e Sites Ilimitados", "unlimited_miu": "MIU Ilimitado", "unlimited_projects": "Projetos Ilimitados", "unlimited_responses": "Respostas Ilimitadas", @@ -1243,6 +1221,7 @@ "copy_survey_description": "Copiar essa pesquisa para outro ambiente", "copy_survey_error": "Falha ao copiar pesquisa", "copy_survey_link_to_clipboard": "Copiar link da pesquisa para a área de transferência", + "copy_survey_partially_success": "{success} pesquisas copiadas com sucesso, {error} falharam.", "copy_survey_success": "Pesquisa copiada com sucesso!", "delete_survey_and_responses_warning": "Você tem certeza de que quer deletar essa pesquisa e todas as suas respostas?", "edit": { diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json index ee9b381b65..2c3e699140 100644 --- a/apps/web/locales/pt-PT.json +++ b/apps/web/locales/pt-PT.json @@ -980,43 +980,29 @@ "api_keys_description": "Gerir chaves API para aceder às APIs de gestão do Formbricks" }, "billing": { - "10000_monthly_responses": "10000 Respostas Mensais", "1000_monthly_responses": "1000 Respostas Mensais", - "1500_monthly_responses": "1500 Respostas Mensais", "1_project": "1 Projeto", "2000_contacts": "2,000 Contactos", - "2000_monthly_identified_users": "2000 Utilizadores Identificados Mensalmente", - "30000_monthly_identified_users": "30000 Utilizadores Identificados Mensalmente", "3_projects": "3 Projetos", "5000_monthly_responses": "5,000 Respostas Mensais", - "5_projects": "5 Projetos", "7500_contacts": "7,500 Contactos", - "7500_monthly_identified_users": "7500 Utilizadores Identificados Mensalmente", - "advanced_targeting": "Segmentação Avançada", "all_integrations": "Todas as Integrações", - "all_surveying_features": "Todas as funcionalidades de inquérito", "annually": "Anualmente", "api_webhooks": "API e Webhooks", "app_surveys": "Inquéritos da Aplicação", "attribute_based_targeting": "Segmentação Baseada em Atributos", - "contact_us": "Contacte-nos", "current": "Atual", "current_plan": "Plano Atual", "current_tier_limit": "Limite Atual do Nível", "custom": "Personalizado e Escala", "custom_contacts_limit": "Limite de Contactos Personalizado", - "custom_miu_limit": "Limite MIU Personalizado", "custom_project_limit": "Limite de Projeto Personalizado", "custom_response_limit": "Limite de Resposta Personalizado", - "customer_success_manager": "Gestor de Sucesso do Cliente", "email_embedded_surveys": "Inquéritos Incorporados no Email", "email_follow_ups": "Acompanhamentos por Email", - "email_support": "Suporte por Email", - "enterprise": "Empresa", "enterprise_description": "Suporte premium e limites personalizados.", "everybody_has_the_free_plan_by_default": "Todos têm o plano gratuito por defeito!", "everything_in_free": "Tudo em Gratuito", - "everything_in_scale": "Tudo em Escala", "everything_in_startup": "Tudo em Startup", "free": "Grátis", "free_description": "Inquéritos ilimitados, membros da equipa e mais.", @@ -1030,25 +1016,17 @@ "manage_subscription": "Gerir Subscrição", "monthly": "Mensal", "monthly_identified_users": "Utilizadores Identificados Mensalmente", - "multi_language_surveys": "Inquéritos Multilingues", "per_month": "por mês", "per_year": "por ano", "plan_upgraded_successfully": "Plano atualizado com sucesso", "premium_support_with_slas": "Suporte premium com SLAs", - "priority_support": "Suporte Prioritário", "remove_branding": "Remover Marca", - "say_hi": "Diga Olá!", - "scale": "Escala", - "scale_and_enterprise": "Escala e Empresa", - "scale_description": "Funcionalidades avançadas para escalar o seu negócio.", "startup": "Inicialização", "startup_description": "Tudo no plano Gratuito com funcionalidades adicionais.", "switch_plan": "Mudar Plano", "switch_plan_confirmation_text": "Tem a certeza de que deseja mudar para o plano {plan}? Ser-lhe-á cobrado {price} {period}.", "team_access_roles": "Funções de Acesso da Equipa", - "technical_onboarding": "Integração Técnica", "unable_to_upgrade_plan": "Não é possível atualizar o plano", - "unlimited_apps_websites": "Aplicações e Websites Ilimitados", "unlimited_miu": "MIU Ilimitado", "unlimited_projects": "Projetos Ilimitados", "unlimited_responses": "Respostas Ilimitadas", @@ -1243,6 +1221,7 @@ "copy_survey_description": "Copiar este questionário para outro ambiente", "copy_survey_error": "Falha ao copiar inquérito", "copy_survey_link_to_clipboard": "Copiar link do inquérito para a área de transferência", + "copy_survey_partially_success": "{success} inquéritos copiados com sucesso, {error} falharam.", "copy_survey_success": "Inquérito copiado com sucesso!", "delete_survey_and_responses_warning": "Tem a certeza de que deseja eliminar este inquérito e todas as suas respostas?", "edit": { diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json index 36192886f4..161e5fc754 100644 --- a/apps/web/locales/zh-Hant-TW.json +++ b/apps/web/locales/zh-Hant-TW.json @@ -980,43 +980,29 @@ "api_keys_description": "管理 API 金鑰以存取 Formbricks 管理 API" }, "billing": { - "10000_monthly_responses": "10000 個每月回應", "1000_monthly_responses": "1000 個每月回應", - "1500_monthly_responses": "1500 個每月回應", "1_project": "1 個專案", "2000_contacts": "2000 個聯絡人", - "2000_monthly_identified_users": "2000 個每月識別使用者", - "30000_monthly_identified_users": "30000 個每月識別使用者", "3_projects": "3 個專案", "5000_monthly_responses": "5000 個每月回應", - "5_projects": "5 個專案", "7500_contacts": "7500 個聯絡人", - "7500_monthly_identified_users": "7500 個每月識別使用者", - "advanced_targeting": "進階目標設定", "all_integrations": "所有整合", - "all_surveying_features": "所有調查功能", "annually": "每年", "api_webhooks": "API 和 Webhook", "app_surveys": "應用程式問卷", "attribute_based_targeting": "基於屬性的定位", - "contact_us": "聯絡我們", "current": "目前", "current_plan": "目前方案", "current_tier_limit": "目前層級限制", "custom": "自訂 & 規模", "custom_contacts_limit": "自訂聯絡人上限", - "custom_miu_limit": "自訂 MIU 上限", "custom_project_limit": "自訂專案上限", "custom_response_limit": "自訂回應上限", - "customer_success_manager": "客戶成功經理", "email_embedded_surveys": "電子郵件嵌入式問卷", "email_follow_ups": "電子郵件後續追蹤", - "email_support": "電子郵件支援", - "enterprise": "企業版", "enterprise_description": "頂級支援和自訂限制。", "everybody_has_the_free_plan_by_default": "每個人預設都有免費方案!", "everything_in_free": "免費方案中的所有功能", - "everything_in_scale": "進階方案中的所有功能", "everything_in_startup": "啟動方案中的所有功能", "free": "免費", "free_description": "無限問卷、團隊成員等。", @@ -1030,25 +1016,17 @@ "manage_subscription": "管理訂閱", "monthly": "每月", "monthly_identified_users": "每月識別使用者", - "multi_language_surveys": "多語言問卷", "per_month": "每月", "per_year": "每年", "plan_upgraded_successfully": "方案已成功升級", "premium_support_with_slas": "具有 SLA 的頂級支援", - "priority_support": "優先支援", "remove_branding": "移除品牌", - "say_hi": "打個招呼!", - "scale": "進階版", - "scale_and_enterprise": "規模 & 企業版", - "scale_description": "用於擴展業務的進階功能。", "startup": "啟動版", "startup_description": "免費方案中的所有功能以及其他功能。", "switch_plan": "切換方案", "switch_plan_confirmation_text": "您確定要切換到 {plan} 計劃嗎?您將被收取 {price} {period}。", "team_access_roles": "團隊存取角色", - "technical_onboarding": "技術新手上路", "unable_to_upgrade_plan": "無法升級方案", - "unlimited_apps_websites": "無限應用程式和網站", "unlimited_miu": "無限 MIU", "unlimited_projects": "無限專案", "unlimited_responses": "無限回應", @@ -1243,6 +1221,7 @@ "copy_survey_description": "將此問卷複製到另一個環境", "copy_survey_error": "無法複製問卷", "copy_survey_link_to_clipboard": "將問卷連結複製到剪貼簿", + "copy_survey_partially_success": "{success} 個問卷已成功複製,{error} 個失敗。", "copy_survey_success": "問卷已成功複製!", "delete_survey_and_responses_warning": "您確定要刪除此問卷及其所有回應嗎?", "edit": { diff --git a/apps/web/modules/survey/list/components/copy-survey-form.test.tsx b/apps/web/modules/survey/list/components/copy-survey-form.test.tsx index d42cf6727d..142a786f9c 100644 --- a/apps/web/modules/survey/list/components/copy-survey-form.test.tsx +++ b/apps/web/modules/survey/list/components/copy-survey-form.test.tsx @@ -1,13 +1,14 @@ import { copySurveyToOtherEnvironmentAction } from "@/modules/survey/list/actions"; import { TUserProject } from "@/modules/survey/list/types/projects"; -import { cleanup, render, screen } from "@testing-library/react"; +import { cleanup, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; +import toast from "react-hot-toast"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { CopySurveyForm } from "./copy-survey-form"; // Mock dependencies vi.mock("@/modules/survey/list/actions", () => ({ - copySurveyToOtherEnvironmentAction: vi.fn().mockResolvedValue({}), + copySurveyToOtherEnvironmentAction: vi.fn(), })); vi.mock("react-hot-toast", () => ({ @@ -19,21 +20,40 @@ vi.mock("react-hot-toast", () => ({ vi.mock("@tolgee/react", () => ({ useTranslate: () => ({ - t: (key: string) => key, + t: (key: string, params?: any) => { + if (key === "environments.surveys.copy_survey_partially_success") { + return `Partially successful: ${params?.success} success, ${params?.error} error`; + } + return key; + }, }), })); -// Mock the Checkbox component to properly handle form changes +vi.mock("@/lib/utils/helper", () => ({ + getFormattedErrorMessage: vi.fn((result) => { + if (result?.serverError) return result.serverError; + if (result?.validationErrors) return "Validation error"; + return "Unknown error"; + }), +})); + +// Mock the form components to make them testable +vi.mock("@/modules/ui/components/form", () => ({ + FormProvider: ({ children }: any) =>
{children}
, + FormField: ({ children, render }: any) => ( +
{render({ field: { value: [], onChange: vi.fn() } })}
+ ), + FormItem: ({ children }: any) =>
{children}
, + FormControl: ({ children }: any) =>
{children}
, +})); + vi.mock("@/modules/ui/components/checkbox", () => ({ Checkbox: ({ id, onCheckedChange, ...props }: any) => ( { - // Call onCheckedChange with the checked state onCheckedChange && onCheckedChange(e.target.checked); }} {...props} @@ -54,10 +74,47 @@ vi.mock("@/modules/ui/components/button", () => ({ ), })); +vi.mock("@/modules/ui/components/label", () => ({ + Label: ({ children, htmlFor }: any) => , +})); + +// Create a mock submit handler +let mockSubmitHandler: any = null; + +// Mock react-hook-form +vi.mock("react-hook-form", () => ({ + useForm: () => ({ + control: {}, + handleSubmit: (fn: any) => { + mockSubmitHandler = fn; + return (e: any) => { + e.preventDefault(); + // Simulate form data with selected environments + const mockFormData = { + projects: [ + { + project: "project-1", + environments: ["env-2"], // Only env-2 selected + }, + { + project: "project-2", + environments: ["env-3"], // Only env-3 selected + }, + ], + }; + return fn(mockFormData); + }; + }, + }), + useFieldArray: () => ({ + fields: [{ project: "project-1" }, { project: "project-2" }], + }), +})); + // Mock data const mockSurvey = { id: "survey-1", - name: "mockSurvey", + name: "Test Survey", type: "link", createdAt: new Date(), updatedAt: new Date(), @@ -90,11 +147,16 @@ const mockProjects = [ describe("CopySurveyForm", () => { const mockSetOpen = vi.fn(); const mockOnCancel = vi.fn(); - const user = userEvent.setup(); + const mockOnSurveysCopied = vi.fn(); + + // Get references to the mocked functions + const mockCopySurveyAction = vi.mocked(copySurveyToOtherEnvironmentAction); + const mockToastSuccess = vi.mocked(toast.success); + const mockToastError = vi.mocked(toast.error); beforeEach(() => { vi.clearAllMocks(); - vi.mocked(copySurveyToOtherEnvironmentAction).mockResolvedValue({ data: { id: "new-survey-id" } }); + mockSubmitHandler = null; }); afterEach(() => { @@ -111,22 +173,14 @@ describe("CopySurveyForm", () => { /> ); - // Check if project names are rendered expect(screen.getByText("Project 1")).toBeInTheDocument(); expect(screen.getByText("Project 2")).toBeInTheDocument(); - - // Check if environment types are rendered - expect(screen.getAllByText("development").length).toBe(2); + expect(screen.getAllByText("development").length).toBe(1); expect(screen.getAllByText("production").length).toBe(2); - - // Check if checkboxes are rendered for each environment - expect(screen.getByTestId("env-1")).toBeInTheDocument(); - expect(screen.getByTestId("env-2")).toBeInTheDocument(); - expect(screen.getByTestId("env-3")).toBeInTheDocument(); - expect(screen.getByTestId("env-4")).toBeInTheDocument(); }); test("calls onCancel when cancel button is clicked", async () => { + const user = userEvent.setup(); render( { expect(mockOnCancel).toHaveBeenCalledTimes(1); }); - test("toggles environment selection when checkbox is clicked", async () => { - render( - - ); + describe("onSubmit function", () => { + test("should handle successful operations", async () => { + mockCopySurveyAction.mockResolvedValue({ + data: { id: "new-survey-1", environmentId: "env-2" }, + }); - // Select multiple environments - await user.click(screen.getByTestId("env-2")); - await user.click(screen.getByTestId("env-3")); + render( + + ); - // Submit the form - await user.click(screen.getByTestId("button-submit")); + // Call the submit handler directly + const mockFormData = { + projects: [ + { + project: "project-1", + environments: ["env-2"], + }, + { + project: "project-2", + environments: ["env-3"], + }, + ], + }; - // Just verify the form can be submitted (integration testing is complex with mocked components) - expect(screen.getByTestId("button-submit")).toBeInTheDocument(); - }); + await mockSubmitHandler(mockFormData); - test("submits form with selected environments", async () => { - render( - - ); + await waitFor(() => { + expect(mockCopySurveyAction).toHaveBeenCalledTimes(2); + expect(mockCopySurveyAction).toHaveBeenCalledWith({ + environmentId: "env-1", + surveyId: "survey-1", + targetEnvironmentId: "env-2", + }); + expect(mockCopySurveyAction).toHaveBeenCalledWith({ + environmentId: "env-1", + surveyId: "survey-1", + targetEnvironmentId: "env-3", + }); + expect(mockToastSuccess).toHaveBeenCalledWith("environments.surveys.copy_survey_success"); + expect(mockSetOpen).toHaveBeenCalledWith(false); + }); + }); - // Select environments - await user.click(screen.getByTestId("env-2")); - await user.click(screen.getByTestId("env-4")); + test("should handle partial success with mixed results", async () => { + mockCopySurveyAction + .mockResolvedValueOnce({ data: { id: "new-survey-1", environmentId: "env-2" } }) + .mockResolvedValueOnce({ serverError: "Failed to copy" }); - // Submit the form - await user.click(screen.getByTestId("button-submit")); + render( + + ); - // Just verify basic form functionality (complex integration testing with mocked components is challenging) - expect(screen.getByTestId("button-submit")).toBeInTheDocument(); + const mockFormData = { + projects: [ + { + project: "project-1", + environments: ["env-2"], + }, + { + project: "project-2", + environments: ["env-3"], + }, + ], + }; + + await mockSubmitHandler(mockFormData); + + await waitFor(() => { + expect(mockToastError).toHaveBeenCalledWith( + "Partially successful: 1 success, 1 error", + expect.objectContaining({ + icon: expect.anything(), + }) + ); + expect(mockToastError).toHaveBeenCalledWith( + "[Project 2] - [development] - Failed to copy", + expect.objectContaining({ + duration: 2000, + }) + ); + expect(mockSetOpen).toHaveBeenCalledWith(false); + }); + }); + + test("should handle all failed operations", async () => { + mockCopySurveyAction + .mockResolvedValueOnce({ serverError: "Server error 1" }) + .mockResolvedValueOnce({ validationErrors: { surveyId: { _errors: ["Invalid survey ID"] } } }); + + render( + + ); + + const mockFormData = { + projects: [ + { + project: "project-1", + environments: ["env-2"], + }, + { + project: "project-2", + environments: ["env-3"], + }, + ], + }; + + await mockSubmitHandler(mockFormData); + + await waitFor(() => { + expect(mockToastError).toHaveBeenCalledWith( + "[Project 1] - [production] - Server error 1", + expect.objectContaining({ + duration: 2000, + }) + ); + expect(mockToastError).toHaveBeenCalledWith( + "[Project 2] - [development] - Validation error", + expect.objectContaining({ + duration: 4000, + }) + ); + expect(mockToastSuccess).not.toHaveBeenCalled(); + expect(mockSetOpen).toHaveBeenCalledWith(false); + }); + }); + + test("should handle exceptions during form submission", async () => { + mockCopySurveyAction.mockRejectedValue(new Error("Network error")); + + render( + + ); + + const mockFormData = { + projects: [ + { + project: "project-1", + environments: ["env-2"], + }, + ], + }; + + await mockSubmitHandler(mockFormData); + + await waitFor(() => { + expect(mockToastError).toHaveBeenCalledWith("environments.surveys.copy_survey_error"); + expect(mockSetOpen).toHaveBeenCalledWith(false); + }); + }); + + test("should handle staggered error toast durations", async () => { + mockCopySurveyAction + .mockResolvedValueOnce({ serverError: "Error 1" }) + .mockResolvedValueOnce({ serverError: "Error 2" }) + .mockResolvedValueOnce({ serverError: "Error 3" }); + + render( + + ); + + const mockFormData = { + projects: [ + { + project: "project-1", + environments: ["env-2"], + }, + { + project: "project-2", + environments: ["env-3", "env-4"], + }, + ], + }; + + await mockSubmitHandler(mockFormData); + + await waitFor(() => { + expect(mockToastError).toHaveBeenCalledWith( + "[Project 1] - [production] - Error 1", + expect.objectContaining({ duration: 2000 }) + ); + expect(mockToastError).toHaveBeenCalledWith( + "[Project 2] - [development] - Error 2", + expect.objectContaining({ duration: 4000 }) + ); + expect(mockToastError).toHaveBeenCalledWith( + "[Project 2] - [production] - Error 3", + expect.objectContaining({ duration: 6000 }) + ); + }); + }); + + test("should not call onSurveysCopied when it's not provided", async () => { + mockCopySurveyAction.mockResolvedValue({ + data: { id: "new-survey-1", environmentId: "env-1" }, + }); + + render( + + ); + + const mockFormData = { + projects: [ + { + project: "project-1", + environments: ["env-2"], + }, + ], + }; + + await mockSubmitHandler(mockFormData); + + await waitFor(() => { + expect(mockSetOpen).toHaveBeenCalledWith(false); + // Should not throw an error even when onSurveysCopied is not provided + }); + }); }); }); diff --git a/apps/web/modules/survey/list/components/copy-survey-form.tsx b/apps/web/modules/survey/list/components/copy-survey-form.tsx index 64318f5396..a421c0e4fb 100644 --- a/apps/web/modules/survey/list/components/copy-survey-form.tsx +++ b/apps/web/modules/survey/list/components/copy-survey-form.tsx @@ -10,22 +10,100 @@ import { FormControl, FormField, FormItem, FormProvider } from "@/modules/ui/com import { Label } from "@/modules/ui/components/label"; import { zodResolver } from "@hookform/resolvers/zod"; import { useTranslate } from "@tolgee/react"; +import { AlertCircleIcon } from "lucide-react"; import { useFieldArray, useForm } from "react-hook-form"; import toast from "react-hot-toast"; -interface ICopySurveyFormProps { - defaultProjects: TUserProject[]; - survey: TSurvey; - onCancel: () => void; - setOpen: (value: boolean) => void; +interface CopySurveyFormProps { + readonly defaultProjects: TUserProject[]; + readonly survey: TSurvey; + readonly onCancel: () => void; + readonly setOpen: (value: boolean) => void; } -export const CopySurveyForm = ({ defaultProjects, survey, onCancel, setOpen }: ICopySurveyFormProps) => { +interface EnvironmentCheckboxProps { + readonly environmentId: string; + readonly environmentType: string; + readonly fieldValue: string[]; + readonly onChange: (value: string[]) => void; +} + +function EnvironmentCheckbox({ + environmentId, + environmentType, + fieldValue, + onChange, +}: EnvironmentCheckboxProps) { + const handleCheckedChange = () => { + if (fieldValue.includes(environmentId)) { + onChange(fieldValue.filter((id) => id !== environmentId)); + } else { + onChange([...fieldValue, environmentId]); + } + }; + + return ( + +
+ +
+ + +
+
+
+
+ ); +} + +interface EnvironmentCheckboxGroupProps { + readonly project: TUserProject; + readonly form: ReturnType>; + readonly projectIndex: number; +} + +function EnvironmentCheckboxGroup({ project, form, projectIndex }: EnvironmentCheckboxGroupProps) { + return ( +
+ {project.environments.map((environment) => ( + ( + + )} + /> + ))} +
+ ); +} + +export const CopySurveyForm = ({ defaultProjects, survey, onCancel, setOpen }: CopySurveyFormProps) => { const { t } = useTranslate(); + + const filteredProjects = defaultProjects.map((project) => ({ + ...project, + environments: project.environments.filter((env) => env.id !== survey.environmentId), + })); + const form = useForm({ resolver: zodResolver(ZSurveyCopyFormValidation), defaultValues: { - projects: defaultProjects.map((project) => ({ + projects: filteredProjects.map((project) => ({ project: project.id, environments: [], })), @@ -37,32 +115,79 @@ export const CopySurveyForm = ({ defaultProjects, survey, onCancel, setOpen }: I control: form.control, }); - const onSubmit = async (data: TSurveyCopyFormData) => { + async function onSubmit(data: TSurveyCopyFormData) { const filteredData = data.projects.filter((project) => project.environments.length > 0); try { - filteredData.forEach(async (project) => { - project.environments.forEach(async (environment) => { - const result = await copySurveyToOtherEnvironmentAction({ - environmentId: survey.environmentId, - surveyId: survey.id, - targetEnvironmentId: environment, - }); + const copyOperationsWithMetadata = filteredData.flatMap((projectData) => { + const project = filteredProjects.find((p) => p.id === projectData.project); + return projectData.environments.map((environmentId) => { + const environment = + project?.environments[0]?.id === environmentId + ? project?.environments[0] + : project?.environments[1]; - if (result?.data) { - toast.success(t("environments.surveys.copy_survey_success")); - } else { - const errorMessage = getFormattedErrorMessage(result); - toast.error(errorMessage); - } + return { + operation: copySurveyToOtherEnvironmentAction({ + environmentId: survey.environmentId, + surveyId: survey.id, + targetEnvironmentId: environmentId, + }), + projectName: project?.name ?? "Unknown Project", + environmentType: environment?.type ?? "unknown", + environmentId, + }; }); }); + + const results = await Promise.all(copyOperationsWithMetadata.map((item) => item.operation)); + + let successCount = 0; + let errorCount = 0; + const errorsIndexes: number[] = []; + + results.forEach((result, index) => { + if (result?.data) { + successCount++; + } else { + errorsIndexes.push(index); + errorCount++; + } + }); + + if (successCount > 0) { + if (errorCount === 0) { + toast.success(t("environments.surveys.copy_survey_success")); + } else { + toast.error( + t("environments.surveys.copy_survey_partially_success", { + success: successCount, + error: errorCount, + }), + { + icon: , + } + ); + } + } + + if (errorsIndexes.length > 0) { + errorsIndexes.forEach((index, idx) => { + const { projectName, environmentType } = copyOperationsWithMetadata[index]; + const result = results[index]; + + const errorMessage = getFormattedErrorMessage(result); + toast.error(`[${projectName}] - [${environmentType}] - ${errorMessage}`, { + duration: 2000 + 2000 * idx, + }); + }); + } } catch (error) { toast.error(t("environments.surveys.copy_survey_error")); } finally { setOpen(false); } - }; + } return ( @@ -71,58 +196,16 @@ export const CopySurveyForm = ({ defaultProjects, survey, onCancel, setOpen }: I className="relative flex h-full w-full flex-col gap-8 overflow-y-auto bg-white p-4">
{formFields.fields.map((field, projectIndex) => { - const project = defaultProjects.find((project) => project.id === field.project); + const project = filteredProjects.find((project) => project.id === field.project); + if (!project) return null; return ( -
+
-

{project?.name}

-
- -
- {project?.environments.map((environment) => { - return ( - { - return ( - -
- - <> - { - if (field.value.includes(environment.id)) { - field.onChange( - field.value.filter((id: string) => id !== environment.id) - ); - } else { - field.onChange([...field.value, environment.id]); - } - }} - className="mr-2 h-4 w-4 appearance-none border-slate-300 checked:border-transparent checked:bg-slate-500 checked:after:bg-slate-500 checked:hover:bg-slate-500 focus:ring-2 focus:ring-slate-500 focus:ring-opacity-50" - id={environment.id} - /> - - - -
-
- ); - }} - /> - ); - })} +

{project.name}

+
); @@ -133,7 +216,6 @@ export const CopySurveyForm = ({ defaultProjects, survey, onCancel, setOpen }: I -
diff --git a/apps/web/modules/survey/list/components/survey-card.tsx b/apps/web/modules/survey/list/components/survey-card.tsx index 20df7c1818..8014cc0bc1 100644 --- a/apps/web/modules/survey/list/components/survey-card.tsx +++ b/apps/web/modules/survey/list/components/survey-card.tsx @@ -17,9 +17,9 @@ interface SurveyCardProps { environmentId: string; isReadOnly: boolean; publicDomain: string; - duplicateSurvey: (survey: TSurvey) => void; deleteSurvey: (surveyId: string) => void; locale: TUserLocale; + onSurveysCopied?: () => void; } export const SurveyCard = ({ survey, @@ -27,8 +27,8 @@ export const SurveyCard = ({ isReadOnly, publicDomain, deleteSurvey, - duplicateSurvey, locale, + onSurveysCopied, }: SurveyCardProps) => { const { t } = useTranslate(); const surveyStatusLabel = (() => { @@ -106,8 +106,8 @@ export const SurveyCard = ({ disabled={isDraftAndReadOnly} refreshSingleUseId={refreshSingleUseId} isSurveyCreationDeletionDisabled={isSurveyCreationDeletionDisabled} - duplicateSurvey={duplicateSurvey} deleteSurvey={deleteSurvey} + onSurveysCopied={onSurveysCopied} /> diff --git a/apps/web/modules/survey/list/components/survey-dropdown-menu.test.tsx b/apps/web/modules/survey/list/components/survey-dropdown-menu.test.tsx index 906fb4690f..97c94c2a5b 100644 --- a/apps/web/modules/survey/list/components/survey-dropdown-menu.test.tsx +++ b/apps/web/modules/survey/list/components/survey-dropdown-menu.test.tsx @@ -86,7 +86,6 @@ describe("SurveyDropDownMenu", () => { survey={{ ...fakeSurvey, status: "completed" }} publicDomain="http://survey.test" refreshSingleUseId={mockRefresh} - duplicateSurvey={mockDuplicateSurvey} deleteSurvey={mockDeleteSurvey} /> ); @@ -118,7 +117,6 @@ describe("SurveyDropDownMenu", () => { survey={fakeSurvey} publicDomain="http://survey.test" refreshSingleUseId={vi.fn()} - duplicateSurvey={vi.fn()} deleteSurvey={vi.fn()} disabled={false} isSurveyCreationDeletionDisabled={false} @@ -158,7 +156,6 @@ describe("SurveyDropDownMenu", () => { survey={fakeSurvey} publicDomain="http://survey.test" refreshSingleUseId={vi.fn()} - duplicateSurvey={vi.fn()} deleteSurvey={vi.fn()} /> ); @@ -181,7 +178,6 @@ describe("SurveyDropDownMenu", () => { survey={{ ...fakeSurvey, responseCount: 0 }} publicDomain="http://survey.test" refreshSingleUseId={vi.fn()} - duplicateSurvey={vi.fn()} deleteSurvey={vi.fn()} /> ); @@ -200,14 +196,12 @@ describe("SurveyDropDownMenu", () => { }); test(" renders and triggers actions correctly", async () => { - const mockDuplicateSurvey = vi.fn(); render( ); @@ -220,21 +214,15 @@ describe("SurveyDropDownMenu", () => { const duplicateButton = screen.getByText("common.duplicate"); expect(duplicateButton).toBeInTheDocument(); await userEvent.click(duplicateButton); - - await waitFor(() => { - expect(mockDuplicateSurvey).toHaveBeenCalled(); - }); }); test(" displays and handles actions correctly", async () => { - const mockDuplicateSurvey = vi.fn(); render( ); @@ -260,10 +248,6 @@ describe("SurveyDropDownMenu", () => { const duplicateButton = screen.getByRole("button", { name: "common.duplicate" }); expect(duplicateButton).toBeInTheDocument(); await userEvent.click(duplicateButton); - - await waitFor(() => { - expect(mockDuplicateSurvey).toHaveBeenCalled(); - }); }); describe("handleDeleteSurvey", () => { @@ -281,7 +265,6 @@ describe("SurveyDropDownMenu", () => { survey={fakeSurvey} publicDomain="http://survey.test" refreshSingleUseId={vi.fn()} - duplicateSurvey={vi.fn()} deleteSurvey={mockDeleteSurvey} /> ); @@ -317,7 +300,6 @@ describe("SurveyDropDownMenu", () => { survey={fakeSurvey} publicDomain="http://survey.test" refreshSingleUseId={vi.fn()} - duplicateSurvey={vi.fn()} deleteSurvey={mockDeleteSurvey} /> ); @@ -354,7 +336,6 @@ describe("SurveyDropDownMenu", () => { survey={fakeSurvey} publicDomain="http://survey.test" refreshSingleUseId={vi.fn()} - duplicateSurvey={vi.fn()} deleteSurvey={mockDeleteSurvey} /> ); @@ -391,7 +372,6 @@ describe("SurveyDropDownMenu", () => { survey={fakeSurvey} publicDomain="http://survey.test" refreshSingleUseId={vi.fn()} - duplicateSurvey={vi.fn()} deleteSurvey={mockDeleteSurvey} /> ); @@ -429,7 +409,6 @@ describe("SurveyDropDownMenu", () => { survey={fakeSurvey} publicDomain="http://survey.test" refreshSingleUseId={vi.fn()} - duplicateSurvey={vi.fn()} deleteSurvey={mockDeleteSurvey} /> ); @@ -484,7 +463,6 @@ describe("SurveyDropDownMenu", () => { survey={fakeSurvey} publicDomain="http://survey.test" refreshSingleUseId={vi.fn()} - duplicateSurvey={vi.fn()} deleteSurvey={mockDeleteSurvey} /> ); diff --git a/apps/web/modules/survey/list/components/survey-dropdown-menu.tsx b/apps/web/modules/survey/list/components/survey-dropdown-menu.tsx index 8f63151653..d6f2a3137a 100644 --- a/apps/web/modules/survey/list/components/survey-dropdown-menu.tsx +++ b/apps/web/modules/survey/list/components/survey-dropdown-menu.tsx @@ -41,8 +41,8 @@ interface SurveyDropDownMenuProps { refreshSingleUseId: () => Promise; disabled?: boolean; isSurveyCreationDeletionDisabled?: boolean; - duplicateSurvey: (survey: TSurvey) => void; deleteSurvey: (surveyId: string) => void; + onSurveysCopied?: () => void; } export const SurveyDropDownMenu = ({ @@ -53,7 +53,7 @@ export const SurveyDropDownMenu = ({ disabled, isSurveyCreationDeletionDisabled, deleteSurvey, - duplicateSurvey, + onSurveysCopied, }: SurveyDropDownMenuProps) => { const { t } = useTranslate(); const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false); @@ -102,13 +102,14 @@ export const SurveyDropDownMenu = ({ surveyId, targetEnvironmentId: environmentId, }); - router.refresh(); if (duplicatedSurveyResponse?.data) { const transformedDuplicatedSurvey = await getSurveyAction({ surveyId: duplicatedSurveyResponse.data.id, }); - if (transformedDuplicatedSurvey?.data) duplicateSurvey(transformedDuplicatedSurvey.data); + if (transformedDuplicatedSurvey?.data) { + onSurveysCopied?.(); + } toast.success(t("environments.surveys.survey_duplicated_successfully")); } else { const errorMessage = getFormattedErrorMessage(duplicatedSurveyResponse); diff --git a/apps/web/modules/survey/list/components/survey-list.test.tsx b/apps/web/modules/survey/list/components/survey-list.test.tsx index 322a2babdc..aa9307070d 100644 --- a/apps/web/modules/survey/list/components/survey-list.test.tsx +++ b/apps/web/modules/survey/list/components/survey-list.test.tsx @@ -341,27 +341,6 @@ describe("SurveysList", () => { }); }); - test("handleDuplicateSurvey adds the duplicated survey to the beginning of the list", async () => { - const initialSurvey = { ...surveyMock, id: "s1", name: "Original Survey" }; - vi.mocked(getSurveysAction).mockResolvedValueOnce({ data: [initialSurvey] }); - const user = userEvent.setup(); - render(); - - await waitFor(() => expect(screen.getByText("Original Survey")).toBeInTheDocument()); - - const duplicateButtonS1 = screen.getByTestId("duplicate-s1"); - // The mock SurveyCard calls duplicateSurvey(survey) with the original survey object. - await user.click(duplicateButtonS1); - - await waitFor(() => { - const surveyCards = screen.getAllByTestId(/survey-card-/); - expect(surveyCards).toHaveLength(2); - // Both cards will show "Original Survey" as the object is prepended. - expect(surveyCards[0]).toHaveTextContent("Original Survey"); - expect(surveyCards[1]).toHaveTextContent("Original Survey"); - }); - }); - test("applies useAutoAnimate ref to the survey list container", async () => { const surveysData = [{ ...surveyMock, id: "s1" }]; vi.mocked(getSurveysAction).mockResolvedValueOnce({ data: surveysData }); diff --git a/apps/web/modules/survey/list/components/survey-list.tsx b/apps/web/modules/survey/list/components/survey-list.tsx index 0a1cbddaf7..343035fb75 100644 --- a/apps/web/modules/survey/list/components/survey-list.tsx +++ b/apps/web/modules/survey/list/components/survey-list.tsx @@ -46,6 +46,7 @@ export const SurveysList = ({ const [surveys, setSurveys] = useState([]); const [isFetching, setIsFetching] = useState(true); const [hasMore, setHasMore] = useState(true); + const [refreshTrigger, setRefreshTrigger] = useState(false); const { t } = useTranslate(); const [surveyFilters, setSurveyFilters] = useState(initialFilters); const [isFilterInitialized, setIsFilterInitialized] = useState(false); @@ -98,7 +99,7 @@ export const SurveysList = ({ }; fetchInitialSurveys(); } - }, [environmentId, surveysLimit, filters, isFilterInitialized]); + }, [environmentId, surveysLimit, filters, isFilterInitialized, refreshTrigger]); const fetchNextPage = useCallback(async () => { setIsFetching(true); @@ -126,10 +127,9 @@ export const SurveysList = ({ if (newSurveys.length === 0) setIsFetching(true); }; - const handleDuplicateSurvey = async (survey: TSurvey) => { - const newSurveys = [survey, ...surveys]; - setSurveys(newSurveys); - }; + const triggerRefresh = useCallback(() => { + setRefreshTrigger((prev) => !prev); + }, []); return (
@@ -158,9 +158,9 @@ export const SurveysList = ({ environmentId={environmentId} isReadOnly={isReadOnly} publicDomain={publicDomain} - duplicateSurvey={handleDuplicateSurvey} deleteSurvey={handleDeleteSurvey} locale={locale} + onSurveysCopied={triggerRefresh} /> ); })}