diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/(workspace)/feedback-sources/page.tsx b/apps/web/app/(app)/workspaces/[workspaceId]/(workspace)/feedback-sources/page.tsx index e70f234fbe..e1e8a8d43e 100644 --- a/apps/web/app/(app)/workspaces/[workspaceId]/(workspace)/feedback-sources/page.tsx +++ b/apps/web/app/(app)/workspaces/[workspaceId]/(workspace)/feedback-sources/page.tsx @@ -1 +1 @@ -export { WorkspaceFeedbackSourcesPage as default } from "@/modules/workspaces/settings/sources/page"; +export { WorkspaceFeedbackSourcesPage as default } from "@/modules/ee/unify-feedback/sources/page"; diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/page.tsx b/apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/page.tsx index fa2de1d503..638e110bcd 100644 --- a/apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/page.tsx +++ b/apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/page.tsx @@ -1,61 +1 @@ -import { notFound } from "next/navigation"; -import { getConnectorsWithMappings } from "@/lib/connector/service"; -import { getTranslate } from "@/lingodotdev/server"; -import { getFeedbackRecordDirectoriesByWorkspaceId } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory"; -import { listFeedbackRecords } from "@/modules/hub/service"; -import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils"; -import { FeedbackRecordsPageClient } from "./components/feedback-records-page-client"; - -const INITIAL_PAGE_SIZE = 50; - -export default async function UnifyFeedbackRecordsPage( - props: Readonly<{ params: Promise<{ workspaceId: string }> }> -) { - const t = await getTranslate(); - const params = await props.params; - - const { isOwner, isManager, hasReadAccess, hasReadWriteAccess, hasManageAccess, session } = - await getWorkspaceAuth(params.workspaceId); - - if (!session) { - throw new Error(t("common.session_not_found")); - } - - const hasAccess = isOwner || isManager || hasReadAccess || hasReadWriteAccess || hasManageAccess; - const canWrite = isOwner || isManager || hasReadWriteAccess || hasManageAccess; - if (!hasAccess) { - return notFound(); - } - - const [frds, connectors] = await Promise.all([ - getFeedbackRecordDirectoriesByWorkspaceId(params.workspaceId), - getConnectorsWithMappings(params.workspaceId), - ]); - - const results = await Promise.all( - frds.map((frd) => listFeedbackRecords({ tenant_id: frd.id, limit: INITIAL_PAGE_SIZE })) - ); - - // Don't crash if Hub is unreachable — show empty state - const successfulResults = results.filter((r) => !r.error); - - const merged = successfulResults - .flatMap((r) => r.data?.data ?? []) - .toSorted((a, b) => (a.collected_at < b.collected_at ? 1 : -1)) - .slice(0, INITIAL_PAGE_SIZE); - - const frdMap = Object.fromEntries(frds.map((f) => [f.id, f.name])); - const csvSources = connectors - .filter((connector) => connector.type === "csv") - .map((connector) => ({ id: connector.id, name: connector.name })); - - return ( - - ); -} +export { UnifyFeedbackRecordsPage as default } from "@/modules/ee/unify-feedback/page"; diff --git a/apps/web/i18n.lock b/apps/web/i18n.lock index eef6ed68cc..6b1107e8b1 100644 --- a/apps/web/i18n.lock +++ b/apps/web/i18n.lock @@ -1767,6 +1767,8 @@ checksums: workspace/analysis/dashboards/no_dashboards_found: e049ec0356009c3a0aa2c729d916efc6 workspace/analysis/dashboards/no_data_message: 464d50cf30281a5b6af2726846eb14b4 workspace/analysis/dashboards/please_enter_name: b9211ed8a0882c0e0109beba48685d68 + workspace/analysis/dashboards/upgrade_prompt_description: 6558780dd4cf1cb2faaa889ed5aaa5e7 + workspace/analysis/dashboards/upgrade_prompt_title: eef434745b5ef7ecc7f49a16e67e5a7a workspace/analysis/manage_feedback_sources: 6aa6a82334ab680b5aa187b7245e8ec8 workspace/analysis/no_feedback_records_message: 67d6ebb9c040304789017d795ca474fc workspace/analysis/no_feedback_records_with_sources_message: 4b72636a55afb4dcf977161ad5a15467 @@ -2347,10 +2349,13 @@ checksums: workspace/settings/billing/trial_feature_api_access: 8c6d03728c3d9470616eb5cee5f9f65d workspace/settings/billing/trial_feature_attribute_segmentation: 90087da973ae48e32ec6d863516fc8c9 workspace/settings/billing/trial_feature_contact_segment_management: 27f17a039ebed6413811ab3a461db2f4 + workspace/settings/billing/trial_feature_dashboards: 04e267e196068634607d701fb7346d30 workspace/settings/billing/trial_feature_email_followups: 0cc02dc14aa28ce94ca6153c306924e5 + workspace/settings/billing/trial_feature_feedback_record_directories: e3d425c27f80162f29ce094e31a3fd8f workspace/settings/billing/trial_feature_hide_branding: b8dbcb24e50e0eb4aeb0c97891cac61d workspace/settings/billing/trial_feature_mobile_sdks: 0963480a27df49657c1b7507adec9a06 workspace/settings/billing/trial_feature_respondent_identification: a82e24ab4c27c5e485326678d9b7bd79 + workspace/settings/billing/trial_feature_unify_feedback: e56481905ea1cf0e47dc30cf0fac6d22 workspace/settings/billing/trial_feature_unlimited_seats: a3257d5b6a23bfbc4b7fd1108087a823 workspace/settings/billing/trial_feature_webhooks: 5ead39fba97fbd37835a476ee67fdd94 workspace/settings/billing/trial_no_credit_card: 01c70aa6e1001815a9a11951394923ca @@ -2465,6 +2470,8 @@ checksums: workspace/settings/feedback_record_directories/title: e3d425c27f80162f29ce094e31a3fd8f workspace/settings/feedback_record_directories/unarchive: 671fc7e9d7c8cb4d182a25a46551c168 workspace/settings/feedback_record_directories/unarchive_workspace_conflict: 82f4b8ebaf41589cfb96e6398dafcc76 + workspace/settings/feedback_record_directories/upgrade_prompt_description: eb8a4bf60bcae458899e1ea94094789d + workspace/settings/feedback_record_directories/upgrade_prompt_title: 8c1c0d72eecf214c9a7aa2054884d51e workspace/settings/feedback_record_directories/workspace_access: 32407b39cf878fb579559c1ed3660892 workspace/settings/general/ai_data_analysis_enabled: 45fabb594da6851f73fef50ca40fe525 workspace/settings/general/ai_data_analysis_enabled_description: 46d4f0bdf4ebf89e78f79cc961a2de83 @@ -3613,6 +3620,8 @@ checksums: workspace/unify/unify_feedback: cd68c8ce0445767e7dcfb4de789903d5 workspace/unify/update_mapping_description: 58d5966c0c9b406c037dff3aa8bcb396 workspace/unify/updated_at: 8fdb85248e591254973403755dcc3724 + workspace/unify/upgrade_prompt_description: 0105e2b7182b9680c179156ce16b6819 + workspace/unify/upgrade_prompt_title: b2c005dc39a22108daf68f9c134bbba9 workspace/unify/upload_csv_data_description: 7fab46222ab05a4424db90a7cc96cdf5 workspace/unify/upload_csv_file: b77797b68cb46a614b3adaa4db24d4c2 workspace/unify/user_identifier: 61073457a5c3901084b557d065f876be diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json index 051cda43ad..928534d4ae 100644 --- a/apps/web/locales/de-DE.json +++ b/apps/web/locales/de-DE.json @@ -1833,7 +1833,9 @@ "failed_to_load_chart_data": "Diagrammdaten konnten nicht geladen werden", "no_dashboards_found": "Keine Dashboards gefunden.", "no_data_message": "Keine Daten. Es gibt derzeit keine Informationen zum Anzeigen. Füge Diagramme hinzu, um dein Dashboard zu erstellen.", - "please_enter_name": "Bitte gib einen Dashboard-Namen ein" + "please_enter_name": "Bitte gib einen Dashboard-Namen ein", + "upgrade_prompt_description": "Erstelle Insights-Dashboards aus deinen Feedback-Daten. Verfügbar in den Pro- und Scale-Plänen.", + "upgrade_prompt_title": "Upgrade durchführen, um Dashboards freizuschalten" }, "manage_feedback_sources": "Manage feedback sources", "no_feedback_records_message": "Sie haben keine Feedback-Datensätze, über die Sie berichten können. Richten Sie Feedbackquellen ein, um Daten in das System einzuspeisen.", @@ -2448,10 +2450,13 @@ "trial_feature_api_access": "API-Zugriff", "trial_feature_attribute_segmentation": "Attributbasierte Segmentierung", "trial_feature_contact_segment_management": "Kontakt- & Segmentverwaltung", + "trial_feature_dashboards": "Insights-Dashboards", "trial_feature_email_followups": "E-Mail-Follow-ups", + "trial_feature_feedback_record_directories": "Feedback-Datensatz-Verzeichnisse", "trial_feature_hide_branding": "Formbricks-Branding ausblenden", "trial_feature_mobile_sdks": "iOS- & Android-SDKs", "trial_feature_respondent_identification": "Teilnehmer-Identifikation", + "trial_feature_unify_feedback": "Unify-Feedback-Posteingang", "trial_feature_unlimited_seats": "Unbegrenzte Plätze", "trial_feature_webhooks": "Benutzerdefinierte Webhooks", "trial_no_credit_card": "14 Tage Testphase, keine Kreditkarte erforderlich", @@ -2572,6 +2577,8 @@ "title": "Feedback-Datensatz-Verzeichnisse", "unarchive": "Aus Archiv wiederherstellen", "unarchive_workspace_conflict": "Dieses Verzeichnis kann nicht wiederhergestellt werden, weil ein oder mehrere zugewiesene Workspaces archiviert sind.", + "upgrade_prompt_description": "Organisiere Feedback-Datensätze in Verzeichnissen und leite Daten zum richtigen Workspace weiter. Verfügbar in den Pro- und Scale-Plänen.", + "upgrade_prompt_title": "Upgrade durchführen, um Feedback-Datensatz-Verzeichnisse freizuschalten", "workspace_access": "Workspace-Zugriff" }, "general": { @@ -3773,6 +3780,8 @@ "unify_feedback": "Feedback vereinheitlichen", "update_mapping_description": "Aktualisiere die Zuordnungskonfiguration für diese Quelle.", "updated_at": "Aktualisiert am", + "upgrade_prompt_description": "Vereinheitliche Kundenfeedback aus jeder Quelle in einem Posteingang. Verfügbar in den Pro- und Scale-Plänen.", + "upgrade_prompt_title": "Upgrade durchführen, um Unify Feedback freizuschalten", "upload_csv_data_description": "Lade eine CSV-Datei hoch, um Feedback-Daten zu importieren.", "upload_csv_file": "CSV-Datei hochladen", "user_identifier": "Benutzer", diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json index e51d654329..f7371c96f5 100644 --- a/apps/web/locales/en-US.json +++ b/apps/web/locales/en-US.json @@ -1833,7 +1833,9 @@ "failed_to_load_chart_data": "Failed to load chart data", "no_dashboards_found": "No dashboards found.", "no_data_message": "No Data. There is currently no information to display. Add charts to build your dashboard.", - "please_enter_name": "Please enter a dashboard name" + "please_enter_name": "Please enter a dashboard name", + "upgrade_prompt_description": "Build insights dashboards from your feedback data. Available on the Pro and Scale plans.", + "upgrade_prompt_title": "Upgrade to unlock Dashboards" }, "manage_feedback_sources": "Manage feedback sources", "no_feedback_records_message": "You don't have Feedback Records to report on. Setup Feedback Sources to feed data into the system.", @@ -2448,10 +2450,13 @@ "trial_feature_api_access": "API Access", "trial_feature_attribute_segmentation": "Attribute-based Segmentation", "trial_feature_contact_segment_management": "Contact & Segment Management", + "trial_feature_dashboards": "Insights Dashboards", "trial_feature_email_followups": "Email Follow-ups", + "trial_feature_feedback_record_directories": "Feedback Record Directories", "trial_feature_hide_branding": "Hide Formbricks Branding", "trial_feature_mobile_sdks": "iOS & Android SDKs", "trial_feature_respondent_identification": "Respondent Identification", + "trial_feature_unify_feedback": "Unify Feedback Inbox", "trial_feature_unlimited_seats": "Unlimited Seats", "trial_feature_webhooks": "Custom Webhooks", "trial_no_credit_card": "14 days trial, no credit card required", @@ -2572,6 +2577,8 @@ "title": "Feedback Record Directories", "unarchive": "Unarchive", "unarchive_workspace_conflict": "Cannot unarchive this directory because one or more assigned workspaces are archived.", + "upgrade_prompt_description": "Organize feedback records into directories and route data to the right workspace. Available on the Pro and Scale plans.", + "upgrade_prompt_title": "Upgrade to unlock Feedback Record Directories", "workspace_access": "Workspace access" }, "general": { @@ -3773,6 +3780,8 @@ "unify_feedback": "Unify Feedback", "update_mapping_description": "Update the mapping configuration for this source.", "updated_at": "Updated at", + "upgrade_prompt_description": "Unify customer feedback from every source in one inbox. Available on the Pro and Scale plans.", + "upgrade_prompt_title": "Upgrade to unlock Unify Feedback", "upload_csv_data_description": "Upload a CSV file to import feedback data.", "upload_csv_file": "Upload CSV File", "user_identifier": "User", diff --git a/apps/web/locales/es-ES.json b/apps/web/locales/es-ES.json index 38ebb5da8f..f5079c5485 100644 --- a/apps/web/locales/es-ES.json +++ b/apps/web/locales/es-ES.json @@ -1833,7 +1833,9 @@ "failed_to_load_chart_data": "Error al cargar los datos del gráfico", "no_dashboards_found": "No se han encontrado paneles de control.", "no_data_message": "Sin datos. Actualmente no hay información que mostrar. Añade gráficos para crear tu panel.", - "please_enter_name": "Por favor, introduce un nombre para el panel de control" + "please_enter_name": "Por favor, introduce un nombre para el panel de control", + "upgrade_prompt_description": "Crea paneles de análisis a partir de tus datos de feedback. Disponible en los planes Pro y Scale.", + "upgrade_prompt_title": "Mejora tu plan para desbloquear los Paneles" }, "manage_feedback_sources": "Manage feedback sources", "no_feedback_records_message": "No tienes registros de comentarios sobre los que informar. Configure fuentes de comentarios para introducir datos en el sistema.", @@ -2448,10 +2450,13 @@ "trial_feature_api_access": "Acceso a la API", "trial_feature_attribute_segmentation": "Segmentación basada en atributos", "trial_feature_contact_segment_management": "Gestión de contactos y segmentos", + "trial_feature_dashboards": "Paneles de Análisis", "trial_feature_email_followups": "Seguimientos por correo electrónico", + "trial_feature_feedback_record_directories": "Directorios de Registros de Feedback", "trial_feature_hide_branding": "Ocultar la marca Formbricks", "trial_feature_mobile_sdks": "SDKs para iOS y Android", "trial_feature_respondent_identification": "Identificación de encuestados", + "trial_feature_unify_feedback": "Bandeja de Entrada Unificada de Feedback", "trial_feature_unlimited_seats": "Asientos ilimitados", "trial_feature_webhooks": "Webhooks personalizados", "trial_no_credit_card": "Prueba de 14 días, sin tarjeta de crédito", @@ -2572,6 +2577,8 @@ "title": "Directorios de Registros de Feedback", "unarchive": "Desarchivar", "unarchive_workspace_conflict": "No se puede desarchivar este directorio porque uno o más espacios de trabajo asignados están archivados.", + "upgrade_prompt_description": "Organiza los registros de feedback en directorios y dirige los datos al espacio de trabajo adecuado. Disponible en los planes Pro y Scale.", + "upgrade_prompt_title": "Mejora tu plan para desbloquear los Directorios de Registros de Feedback", "workspace_access": "Acceso al espacio de trabajo" }, "general": { @@ -3773,6 +3780,8 @@ "unify_feedback": "Unificar feedback", "update_mapping_description": "Actualiza la configuración de mapeo para esta fuente.", "updated_at": "Actualizado el", + "upgrade_prompt_description": "Unifica el feedback de tus clientes de todas las fuentes en una sola bandeja de entrada. Disponible en los planes Pro y Scale.", + "upgrade_prompt_title": "Mejora tu plan para desbloquear Unificar Feedback", "upload_csv_data_description": "Sube un archivo CSV para importar datos de comentarios.", "upload_csv_file": "Subir archivo CSV", "user_identifier": "Usuario", diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json index 43cc6c7037..a8418eb506 100644 --- a/apps/web/locales/fr-FR.json +++ b/apps/web/locales/fr-FR.json @@ -1833,7 +1833,9 @@ "failed_to_load_chart_data": "Échec du chargement des données du graphique", "no_dashboards_found": "Aucun tableau de bord trouvé.", "no_data_message": "Aucune donnée. Il n'y a actuellement aucune information à afficher. Ajoute des graphiques pour construire ton tableau de bord.", - "please_enter_name": "Veuillez saisir un nom de tableau de bord" + "please_enter_name": "Veuillez saisir un nom de tableau de bord", + "upgrade_prompt_description": "Créez des tableaux de bord d'insights à partir de vos données de feedback. Disponible avec les forfaits Pro et Scale.", + "upgrade_prompt_title": "Passez à un forfait supérieur pour débloquer les Tableaux de bord" }, "manage_feedback_sources": "Manage feedback sources", "no_feedback_records_message": "Vous n'avez pas d'enregistrements de commentaires sur lesquels créer des rapports. Configurez des sources de commentaires pour introduire des données dans le système.", @@ -2448,10 +2450,13 @@ "trial_feature_api_access": "Accès API", "trial_feature_attribute_segmentation": "Segmentation basée sur les attributs", "trial_feature_contact_segment_management": "Gestion des contacts et segments", + "trial_feature_dashboards": "Tableaux de bord d'insights", "trial_feature_email_followups": "Relances par e-mail", + "trial_feature_feedback_record_directories": "Répertoires d'enregistrements de feedback", "trial_feature_hide_branding": "Masquer l'image de marque Formbricks", "trial_feature_mobile_sdks": "SDKs iOS et Android", "trial_feature_respondent_identification": "Identification des répondants", + "trial_feature_unify_feedback": "Boîte de réception Unify Feedback", "trial_feature_unlimited_seats": "Places illimitées", "trial_feature_webhooks": "Webhooks personnalisés", "trial_no_credit_card": "Essai de 14 jours, aucune carte bancaire requise", @@ -2572,6 +2577,8 @@ "title": "Répertoires d'enregistrement des retours", "unarchive": "Désarchiver", "unarchive_workspace_conflict": "Impossible de désarchiver ce répertoire, car un ou plusieurs espaces de travail attribués sont archivés.", + "upgrade_prompt_description": "Organisez les enregistrements de feedback dans des répertoires et dirigez les données vers le bon espace de travail. Disponible avec les forfaits Pro et Scale.", + "upgrade_prompt_title": "Passez à un forfait supérieur pour débloquer les Répertoires d'enregistrements de feedback", "workspace_access": "Accès à l’espace de travail" }, "general": { @@ -3773,6 +3780,8 @@ "unify_feedback": "Unifier les retours", "update_mapping_description": "Mettre à jour la configuration de mappage pour cette source.", "updated_at": "Mis à jour à", + "upgrade_prompt_description": "Centralisez tous les feedbacks clients de toutes les sources dans une seule boîte de réception. Disponible avec les forfaits Pro et Scale.", + "upgrade_prompt_title": "Passez à un forfait supérieur pour débloquer Unify Feedback", "upload_csv_data_description": "Téléchargez un fichier CSV pour importer des données de feedback.", "upload_csv_file": "Télécharger un fichier CSV", "user_identifier": "Utilisateur", diff --git a/apps/web/locales/hu-HU.json b/apps/web/locales/hu-HU.json index 9f326eeca2..8e35ef67d7 100644 --- a/apps/web/locales/hu-HU.json +++ b/apps/web/locales/hu-HU.json @@ -1833,7 +1833,9 @@ "failed_to_load_chart_data": "A diagram adatainak betöltése sikertelen", "no_dashboards_found": "Nem található vezérlőpult.", "no_data_message": "Nincsenek adatok. Jelenleg nincsenek megjeleníthető információk. Adjon hozzá diagramokat az irányítópult felépítéséhez.", - "please_enter_name": "Kérjük, adjon nevet a vezérlőpultnak" + "please_enter_name": "Kérjük, adjon nevet a vezérlőpultnak", + "upgrade_prompt_description": "Hozzon létre betekintési irányítópultokat a visszajelzési adataiból. A Pro és Scale csomagokban érhető el.", + "upgrade_prompt_title": "Frissítsen a csomagon, hogy feloldja az Irányítópultokat" }, "manage_feedback_sources": "Manage feedback sources", "no_feedback_records_message": "Nincsenek visszajelzési rekordjai, amelyekről jelentést tehetne. Állítsa be a visszacsatolási forrásokat, hogy adatokat tápláljon be a rendszerbe.", @@ -2448,10 +2450,13 @@ "trial_feature_api_access": "API-hozzáférés", "trial_feature_attribute_segmentation": "Attribútumalapú szakaszolás", "trial_feature_contact_segment_management": "Partner- és szakaszkezelés", + "trial_feature_dashboards": "Betekintési Irányítópultok", "trial_feature_email_followups": "E-mailes utókövetések", + "trial_feature_feedback_record_directories": "Visszajelzési Rekord Könyvtárak", "trial_feature_hide_branding": "Formbricks márkajel elrejtése", "trial_feature_mobile_sdks": "iOS és Android SDK-k", "trial_feature_respondent_identification": "Válaszadó-azonosítás", + "trial_feature_unify_feedback": "Egyesített Visszajelzési Postaláda", "trial_feature_unlimited_seats": "Korlátlan számú hely", "trial_feature_webhooks": "Egyéni webhorgok", "trial_no_credit_card": "14 napos próbaidőszak, nincs szükség hitelkártyára", @@ -2572,6 +2577,8 @@ "title": "Visszajelzési Nyilvántartási Könyvtárak", "unarchive": "Archiválás visszavonása", "unarchive_workspace_conflict": "A könyvtár nem állítható vissza, mert egy vagy több hozzárendelt munkaterület archiválva van.", + "upgrade_prompt_description": "Szervezze a visszajelzési rekordokat könyvtárakba, és irányítsa az adatokat a megfelelő munkaterületre. A Pro és Scale csomagokban érhető el.", + "upgrade_prompt_title": "Frissítsen a csomagon, hogy feloldja a Visszajelzési Rekord Könyvtárakat", "workspace_access": "Munkaterület-hozzáférés" }, "general": { @@ -3773,6 +3780,8 @@ "unify_feedback": "Visszajelzések egyesítése", "update_mapping_description": "Frissítse a leképezési konfigurációt ehhez a forráshoz.", "updated_at": "Frissítve", + "upgrade_prompt_description": "Egyesítse az ügyfelek visszajelzéseit minden forrásból egyetlen postaládában. A Pro és Scale csomagokban érhető el.", + "upgrade_prompt_title": "Frissítsen a csomagon, hogy feloldja az Egyesített Visszajelzési Postaládát", "upload_csv_data_description": "Tölts fel egy CSV fájlt a visszajelzési adatok importálásához.", "upload_csv_file": "CSV fájl feltöltése", "user_identifier": "Felhasználó", diff --git a/apps/web/locales/ja-JP.json b/apps/web/locales/ja-JP.json index 5cc1116f7c..8e805818cc 100644 --- a/apps/web/locales/ja-JP.json +++ b/apps/web/locales/ja-JP.json @@ -1833,7 +1833,9 @@ "failed_to_load_chart_data": "チャートデータの読み込みに失敗しました", "no_dashboards_found": "ダッシュボードが見つかりません。", "no_data_message": "データがありません。現在表示する情報がありません。ダッシュボードを構築するにはチャートを追加してください。", - "please_enter_name": "ダッシュボード名を入力してください" + "please_enter_name": "ダッシュボード名を入力してください", + "upgrade_prompt_description": "フィードバックデータからインサイトダッシュボードを構築できます。ProプランおよびScaleプランでご利用いただけます。", + "upgrade_prompt_title": "アップグレードしてダッシュボードを利用" }, "manage_feedback_sources": "Manage feedback sources", "no_feedback_records_message": "レポートするフィードバック レコードがありません。データをシステムにフィードするためのフィードバック ソースをセットアップします。", @@ -2448,10 +2450,13 @@ "trial_feature_api_access": "APIアクセス", "trial_feature_attribute_segmentation": "属性ベースのセグメンテーション", "trial_feature_contact_segment_management": "連絡先とセグメントの管理", + "trial_feature_dashboards": "インサイトダッシュボード", "trial_feature_email_followups": "メールフォローアップ", + "trial_feature_feedback_record_directories": "フィードバックレコードディレクトリ", "trial_feature_hide_branding": "Formbricksブランディングを非表示", "trial_feature_mobile_sdks": "iOS & Android SDK", "trial_feature_respondent_identification": "回答者の識別", + "trial_feature_unify_feedback": "フィードバック統合インボックス", "trial_feature_unlimited_seats": "無制限のシート数", "trial_feature_webhooks": "カスタムWebhook", "trial_no_credit_card": "14日間トライアル、クレジットカード不要", @@ -2572,6 +2577,8 @@ "title": "フィードバック記録ディレクトリ", "unarchive": "アーカイブ解除", "unarchive_workspace_conflict": "割り当てられているワークスペースの1つ以上がアーカイブされているため、このディレクトリをアーカイブ解除できません。", + "upgrade_prompt_description": "フィードバックレコードをディレクトリで整理し、適切なワークスペースにデータを振り分けられます。ProプランおよびScaleプランでご利用いただけます。", + "upgrade_prompt_title": "アップグレードしてフィードバックレコードディレクトリを利用", "workspace_access": "ワークスペースアクセス" }, "general": { @@ -3773,6 +3780,8 @@ "unify_feedback": "フィードバックを統合", "update_mapping_description": "このソースのマッピング設定を更新します。", "updated_at": "更新日時", + "upgrade_prompt_description": "あらゆるソースからの顧客フィードバックを1つのインボックスに統合できます。ProプランおよびScaleプランでご利用いただけます。", + "upgrade_prompt_title": "アップグレードしてフィードバック統合を利用", "upload_csv_data_description": "CSVファイルをアップロードして、フィードバックデータをインポートします。", "upload_csv_file": "CSVファイルをアップロード", "user_identifier": "ユーザー", diff --git a/apps/web/locales/nl-NL.json b/apps/web/locales/nl-NL.json index 0ffd39dc89..fe04dcba8b 100644 --- a/apps/web/locales/nl-NL.json +++ b/apps/web/locales/nl-NL.json @@ -1833,7 +1833,9 @@ "failed_to_load_chart_data": "Grafiekgegevens laden mislukt", "no_dashboards_found": "Geen dashboards gevonden.", "no_data_message": "Geen gegevens. Er is momenteel geen informatie om weer te geven. Voeg grafieken toe om je dashboard op te bouwen.", - "please_enter_name": "Voer een dashboardnaam in" + "please_enter_name": "Voer een dashboardnaam in", + "upgrade_prompt_description": "Bouw inzichtelijke dashboards op basis van je feedbackgegevens. Beschikbaar op de Pro- en Scale-abonnementen.", + "upgrade_prompt_title": "Upgrade om Dashboards te ontgrendelen" }, "manage_feedback_sources": "Manage feedback sources", "no_feedback_records_message": "U heeft geen feedbackrecords om over te rapporteren. Stel feedbackbronnen in om gegevens in het systeem in te voeren.", @@ -2448,10 +2450,13 @@ "trial_feature_api_access": "API-toegang", "trial_feature_attribute_segmentation": "Segmentatie op basis van attributen", "trial_feature_contact_segment_management": "Contact- en segmentbeheer", + "trial_feature_dashboards": "Inzichten Dashboards", "trial_feature_email_followups": "E-mail follow-ups", + "trial_feature_feedback_record_directories": "Feedbackrecord Mappen", "trial_feature_hide_branding": "Verberg Formbricks-branding", "trial_feature_mobile_sdks": "iOS- en Android-SDK's", "trial_feature_respondent_identification": "Identificatie van respondenten", + "trial_feature_unify_feedback": "Unify Feedback Inbox", "trial_feature_unlimited_seats": "Onbeperkt aantal gebruikers", "trial_feature_webhooks": "Aangepaste webhooks", "trial_no_credit_card": "14 dagen proefperiode, geen creditcard vereist", @@ -2572,6 +2577,8 @@ "title": "Feedbackregistratiemappen", "unarchive": "Dearchiveren", "unarchive_workspace_conflict": "Deze map kan niet worden gedearchiveerd omdat een of meer toegewezen workspaces zijn gearchiveerd.", + "upgrade_prompt_description": "Organiseer feedbackrecords in mappen en routeer gegevens naar de juiste workspace. Beschikbaar op de Pro- en Scale-abonnementen.", + "upgrade_prompt_title": "Upgrade om Feedbackrecord Mappen te ontgrendelen", "workspace_access": "Workspace-toegang" }, "general": { @@ -3773,6 +3780,8 @@ "unify_feedback": "Feedback verenigen", "update_mapping_description": "Werk de mappingconfiguratie voor deze bron bij.", "updated_at": "Bijgewerkt op", + "upgrade_prompt_description": "Bundel klantfeedback uit elke bron in één inbox. Beschikbaar op de Pro- en Scale-abonnementen.", + "upgrade_prompt_title": "Upgrade om Unify Feedback te ontgrendelen", "upload_csv_data_description": "Upload een CSV-bestand om feedbackgegevens te importeren.", "upload_csv_file": "CSV-bestand uploaden", "user_identifier": "Gebruiker", diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json index 418704c6c4..4a60ef0a1e 100644 --- a/apps/web/locales/pt-BR.json +++ b/apps/web/locales/pt-BR.json @@ -1833,7 +1833,9 @@ "failed_to_load_chart_data": "Falha ao carregar os dados do gráfico", "no_dashboards_found": "Nenhum painel encontrado.", "no_data_message": "Sem Dados. Não há informações para exibir no momento. Adicione gráficos para construir seu painel.", - "please_enter_name": "Por favor, digite um nome para o painel" + "please_enter_name": "Por favor, digite um nome para o painel", + "upgrade_prompt_description": "Crie dashboards de insights a partir dos seus dados de feedback. Disponível nos planos Pro e Scale.", + "upgrade_prompt_title": "Faça upgrade para desbloquear Dashboards" }, "manage_feedback_sources": "Manage feedback sources", "no_feedback_records_message": "Você não tem registros de feedback para relatar. Configure fontes de feedback para alimentar dados no sistema.", @@ -2448,10 +2450,13 @@ "trial_feature_api_access": "Acesso à API", "trial_feature_attribute_segmentation": "Segmentação Baseada em Atributos", "trial_feature_contact_segment_management": "Gerenciamento de Contatos e Segmentos", + "trial_feature_dashboards": "Dashboards de Insights", "trial_feature_email_followups": "Follow-ups por E-mail", + "trial_feature_feedback_record_directories": "Diretórios de Registros de Feedback", "trial_feature_hide_branding": "Ocultar Marca Formbricks", "trial_feature_mobile_sdks": "SDKs para iOS e Android", "trial_feature_respondent_identification": "Identificação de Respondentes", + "trial_feature_unify_feedback": "Caixa de Entrada Unificada de Feedback", "trial_feature_unlimited_seats": "Assentos Ilimitados", "trial_feature_webhooks": "Webhooks Personalizados", "trial_no_credit_card": "14 dias de teste, sem necessidade de cartão de crédito", @@ -2572,6 +2577,8 @@ "title": "Diretórios de Registros de Feedback", "unarchive": "Desarquivar", "unarchive_workspace_conflict": "Não é possível desarquivar este diretório porque um ou mais workspaces atribuídos estão arquivados.", + "upgrade_prompt_description": "Organize registros de feedback em diretórios e direcione dados para o workspace certo. Disponível nos planos Pro e Scale.", + "upgrade_prompt_title": "Faça upgrade para desbloquear Diretórios de Registros de Feedback", "workspace_access": "Acesso ao workspace" }, "general": { @@ -3773,6 +3780,8 @@ "unify_feedback": "Unificar feedback", "update_mapping_description": "Atualize a configuração de mapeamento para esta fonte.", "updated_at": "Atualizado em", + "upgrade_prompt_description": "Unifique o feedback dos clientes de todas as fontes em uma única caixa de entrada. Disponível nos planos Pro e Scale.", + "upgrade_prompt_title": "Faça upgrade para desbloquear Unify Feedback", "upload_csv_data_description": "Faça upload de um arquivo CSV para importar dados de feedback.", "upload_csv_file": "Fazer upload de arquivo CSV", "user_identifier": "Usuário", diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json index f815502962..4af3b5bf37 100644 --- a/apps/web/locales/pt-PT.json +++ b/apps/web/locales/pt-PT.json @@ -1833,7 +1833,9 @@ "failed_to_load_chart_data": "Falha ao carregar os dados do gráfico", "no_dashboards_found": "Nenhum painel encontrado.", "no_data_message": "Sem Dados. Atualmente não há informação para apresentar. Adiciona gráficos para construir o teu painel.", - "please_enter_name": "Por favor, introduza um nome para o painel" + "please_enter_name": "Por favor, introduza um nome para o painel", + "upgrade_prompt_description": "Cria dashboards de insights a partir dos teus dados de feedback. Disponível nos planos Pro e Scale.", + "upgrade_prompt_title": "Faz upgrade para desbloquear Dashboards" }, "manage_feedback_sources": "Manage feedback sources", "no_feedback_records_message": "Você não tem registros de feedback para relatar. Configure fontes de feedback para alimentar dados no sistema.", @@ -2448,10 +2450,13 @@ "trial_feature_api_access": "Acesso à API", "trial_feature_attribute_segmentation": "Segmentação Baseada em Atributos", "trial_feature_contact_segment_management": "Gestão de Contactos e Segmentos", + "trial_feature_dashboards": "Dashboards de Insights", "trial_feature_email_followups": "Seguimentos por E-mail", + "trial_feature_feedback_record_directories": "Diretórios de Registos de Feedback", "trial_feature_hide_branding": "Ocultar Marca Formbricks", "trial_feature_mobile_sdks": "SDKs para iOS e Android", "trial_feature_respondent_identification": "Identificação de Inquiridos", + "trial_feature_unify_feedback": "Unify Feedback Inbox", "trial_feature_unlimited_seats": "Lugares Ilimitados", "trial_feature_webhooks": "Webhooks Personalizados", "trial_no_credit_card": "14 dias de teste, sem necessidade de cartão de crédito", @@ -2572,6 +2577,8 @@ "title": "Diretórios de Registos de Feedback", "unarchive": "Desarquivar", "unarchive_workspace_conflict": "Não é possível desarquivar este diretório porque um ou mais workspaces atribuídos estão arquivados.", + "upgrade_prompt_description": "Organiza os registos de feedback em diretórios e encaminha os dados para o workspace certo. Disponível nos planos Pro e Scale.", + "upgrade_prompt_title": "Faz upgrade para desbloquear Diretórios de Registos de Feedback", "workspace_access": "Acesso ao workspace" }, "general": { @@ -3773,6 +3780,8 @@ "unify_feedback": "Unificar feedback", "update_mapping_description": "Atualiza a configuração de mapeamento para esta origem.", "updated_at": "Atualizado em", + "upgrade_prompt_description": "Unifica o feedback dos clientes de todas as fontes numa única inbox. Disponível nos planos Pro e Scale.", + "upgrade_prompt_title": "Faz upgrade para desbloquear Unify Feedback", "upload_csv_data_description": "Carrega um ficheiro CSV para importar dados de feedback.", "upload_csv_file": "Carregar ficheiro CSV", "user_identifier": "Utilizador", diff --git a/apps/web/locales/ro-RO.json b/apps/web/locales/ro-RO.json index 98d9102491..d0da47671e 100644 --- a/apps/web/locales/ro-RO.json +++ b/apps/web/locales/ro-RO.json @@ -1833,7 +1833,9 @@ "failed_to_load_chart_data": "Încărcarea datelor graficului a eșuat", "no_dashboards_found": "Nu s-a găsit niciun tablou de bord.", "no_data_message": "Fără date. În prezent nu există informații de afișat. Adaugă grafice pentru a-ți construi tabloul de bord.", - "please_enter_name": "Te rugăm să introduci un nume pentru tablou de bord" + "please_enter_name": "Te rugăm să introduci un nume pentru tablou de bord", + "upgrade_prompt_description": "Construiește dashboarduri de analiză din datele tale de feedback. Disponibile în planurile Pro și Scale.", + "upgrade_prompt_title": "Actualizează pentru a debloca Dashboardurile" }, "manage_feedback_sources": "Manage feedback sources", "no_feedback_records_message": "Nu aveți înregistrări de feedback despre care să raportați. Configurați sursele de feedback pentru a introduce date în sistem.", @@ -2448,10 +2450,13 @@ "trial_feature_api_access": "Acces API", "trial_feature_attribute_segmentation": "Segmentare bazată pe atribute", "trial_feature_contact_segment_management": "Gestionare contacte și segmente", + "trial_feature_dashboards": "Dashboarduri de Analiză", "trial_feature_email_followups": "Urmăriri prin email", + "trial_feature_feedback_record_directories": "Directoare pentru Înregistrări de Feedback", "trial_feature_hide_branding": "Ascunde branding-ul Formbricks", "trial_feature_mobile_sdks": "SDK-uri iOS și Android", "trial_feature_respondent_identification": "Identificarea respondenților", + "trial_feature_unify_feedback": "Inbox Unificat de Feedback", "trial_feature_unlimited_seats": "Locuri nelimitate", "trial_feature_webhooks": "Webhook-uri personalizate", "trial_no_credit_card": "14 zile de probă, fără card necesar", @@ -2572,6 +2577,8 @@ "title": "Directoare de Înregistrări Feedback", "unarchive": "Dezarhivează", "unarchive_workspace_conflict": "Acest director nu poate fi dezarhivat deoarece unul sau mai multe spații de lucru alocate sunt arhivate.", + "upgrade_prompt_description": "Organizează înregistrările de feedback în directoare și direcționează datele către workspace-ul potrivit. Disponibile în planurile Pro și Scale.", + "upgrade_prompt_title": "Actualizează pentru a debloca Directoarele pentru Înregistrări de Feedback", "workspace_access": "Acces la spațiul de lucru" }, "general": { @@ -3773,6 +3780,8 @@ "unify_feedback": "Unify Feedback", "update_mapping_description": "Actualizează configurația de mapare pentru această sursă.", "updated_at": "Actualizat la", + "upgrade_prompt_description": "Unifică feedback-ul clienților din toate sursele într-un singur inbox. Disponibil în planurile Pro și Scale.", + "upgrade_prompt_title": "Actualizează pentru a debloca Unify Feedback", "upload_csv_data_description": "Încarcă un fișier CSV pentru a importa date de feedback.", "upload_csv_file": "Încarcă fișier CSV", "user_identifier": "Utilizator", diff --git a/apps/web/locales/ru-RU.json b/apps/web/locales/ru-RU.json index 3c3dd6f704..71bcce9392 100644 --- a/apps/web/locales/ru-RU.json +++ b/apps/web/locales/ru-RU.json @@ -1833,7 +1833,9 @@ "failed_to_load_chart_data": "Не удалось загрузить данные графика", "no_dashboards_found": "Панели управления не найдены.", "no_data_message": "Нет данных. В настоящее время нет информации для отображения. Добавьте графики, чтобы построить свой дашборд.", - "please_enter_name": "Пожалуйста, введите название панели управления" + "please_enter_name": "Пожалуйста, введите название панели управления", + "upgrade_prompt_description": "Создавай дашборды с аналитикой на основе данных обратной связи. Доступно в тарифах Pro и Scale.", + "upgrade_prompt_title": "Обнови тариф, чтобы получить доступ к дашбордам" }, "manage_feedback_sources": "Manage feedback sources", "no_feedback_records_message": "У вас нет записей обратной связи, о которых можно сообщить. Настройте источники обратной связи для подачи данных в систему.", @@ -2448,10 +2450,13 @@ "trial_feature_api_access": "Доступ к API", "trial_feature_attribute_segmentation": "Сегментация на основе атрибутов", "trial_feature_contact_segment_management": "Управление контактами и сегментами", + "trial_feature_dashboards": "Дашборды аналитики", "trial_feature_email_followups": "Email-уведомления", + "trial_feature_feedback_record_directories": "Директории записей обратной связи", "trial_feature_hide_branding": "Скрыть брендинг Formbricks", "trial_feature_mobile_sdks": "iOS и Android SDK", "trial_feature_respondent_identification": "Идентификация респондентов", + "trial_feature_unify_feedback": "Единый inbox обратной связи", "trial_feature_unlimited_seats": "Неограниченное количество мест", "trial_feature_webhooks": "Пользовательские вебхуки", "trial_no_credit_card": "14 дней пробного периода, кредитная карта не требуется", @@ -2572,6 +2577,8 @@ "title": "Директории записей обратной связи", "unarchive": "Разархивировать", "unarchive_workspace_conflict": "Невозможно разархивировать этот каталог, потому что один или несколько назначенных рабочих пространств архивированы.", + "upgrade_prompt_description": "Организуй записи обратной связи в директории и направляй данные в нужное рабочее пространство. Доступно в тарифах Pro и Scale.", + "upgrade_prompt_title": "Обнови тариф, чтобы получить доступ к директориям записей обратной связи", "workspace_access": "Доступ к рабочему пространству" }, "general": { @@ -3773,6 +3780,8 @@ "unify_feedback": "Обратная связь Unify", "update_mapping_description": "Обнови настройки сопоставления для этого источника.", "updated_at": "Обновлено", + "upgrade_prompt_description": "Объедини всю обратную связь от клиентов из любых источников в одном inbox. Доступно в тарифах Pro и Scale.", + "upgrade_prompt_title": "Обнови тариф, чтобы получить доступ к Unify Feedback", "upload_csv_data_description": "Загрузи CSV-файл, чтобы импортировать данные отзывов.", "upload_csv_file": "Загрузить CSV-файл", "user_identifier": "Пользователь", diff --git a/apps/web/locales/sv-SE.json b/apps/web/locales/sv-SE.json index 8165e26667..dc5c810c35 100644 --- a/apps/web/locales/sv-SE.json +++ b/apps/web/locales/sv-SE.json @@ -1833,7 +1833,9 @@ "failed_to_load_chart_data": "Det gick inte att ladda diagramdata", "no_dashboards_found": "Inga instrumentpaneler hittades.", "no_data_message": "Ingen data. Det finns för närvarande ingen information att visa. Lägg till diagram för att bygga din instrumentpanel.", - "please_enter_name": "Ange ett namn på instrumentpanelen" + "please_enter_name": "Ange ett namn på instrumentpanelen", + "upgrade_prompt_description": "Skapa insiktsdashboards från din feedbackdata. Tillgängligt på Pro- och Scale-planerna.", + "upgrade_prompt_title": "Uppgradera för att låsa upp Dashboards" }, "manage_feedback_sources": "Manage feedback sources", "no_feedback_records_message": "Du har inga feedbackposter att rapportera om. Ställ in återkopplingskällor för att mata in data i systemet.", @@ -2448,10 +2450,13 @@ "trial_feature_api_access": "API-åtkomst", "trial_feature_attribute_segmentation": "Attributbaserad segmentering", "trial_feature_contact_segment_management": "Kontakt- och segmenthantering", + "trial_feature_dashboards": "Insiktsdashboards", "trial_feature_email_followups": "E-postuppföljningar", + "trial_feature_feedback_record_directories": "Feedbackpostkataloger", "trial_feature_hide_branding": "Dölj Formbricks-branding", "trial_feature_mobile_sdks": "iOS- och Android-SDK:er", "trial_feature_respondent_identification": "Respondentidentifiering", + "trial_feature_unify_feedback": "Unify Feedback-inkorg", "trial_feature_unlimited_seats": "Obegränsade platser", "trial_feature_webhooks": "Anpassade webhooks", "trial_no_credit_card": "14 dagars provperiod, inget kreditkort krävs", @@ -2572,6 +2577,8 @@ "title": "Feedbackkataloger", "unarchive": "Avarkivera", "unarchive_workspace_conflict": "Den här katalogen kan inte avarkiveras eftersom en eller flera tilldelade arbetsytor är arkiverade.", + "upgrade_prompt_description": "Organisera feedbackposter i kataloger och dirigera data till rätt arbetsyta. Tillgängligt på Pro- och Scale-planerna.", + "upgrade_prompt_title": "Uppgradera för att låsa upp Feedbackpostkataloger", "workspace_access": "Arbetsyteåtkomst" }, "general": { @@ -3773,6 +3780,8 @@ "unify_feedback": "Samla feedback", "update_mapping_description": "Uppdatera mappningskonfigurationen för den här källan.", "updated_at": "Uppdaterad", + "upgrade_prompt_description": "Samla kundfeedback från alla källor i en inkorg. Tillgängligt på Pro- och Scale-planerna.", + "upgrade_prompt_title": "Uppgradera för att låsa upp Unify Feedback", "upload_csv_data_description": "Ladda upp en CSV-fil för att importera feedbackdata.", "upload_csv_file": "Ladda upp CSV-fil", "user_identifier": "Användare", diff --git a/apps/web/locales/tr-TR.json b/apps/web/locales/tr-TR.json index baa48acb04..3535cfa68e 100644 --- a/apps/web/locales/tr-TR.json +++ b/apps/web/locales/tr-TR.json @@ -1833,7 +1833,9 @@ "failed_to_load_chart_data": "Grafik verileri yüklenemedi", "no_dashboards_found": "Gösterge paneli bulunamadı.", "no_data_message": "Veri Yok. Şu anda görüntülenecek bilgi bulunmuyor. Gösterge panelini oluşturmak için grafik ekle.", - "please_enter_name": "Lütfen bir gösterge paneli adı gir" + "please_enter_name": "Lütfen bir gösterge paneli adı gir", + "upgrade_prompt_description": "Geri bildirim verilerinizden içgörü panoları oluşturun. Pro ve Scale planlarında kullanılabilir.", + "upgrade_prompt_title": "Panoların Kilidini Açmak İçin Yükseltin" }, "manage_feedback_sources": "Manage feedback sources", "no_feedback_records_message": "Raporlayabileceğiniz Geri Bildirim Kayıtlarınız yok. Verileri sisteme beslemek için Geri Bildirim Kaynaklarını ayarlayın.", @@ -2448,10 +2450,13 @@ "trial_feature_api_access": "API Erişimi", "trial_feature_attribute_segmentation": "Özellik Tabanlı Segmentasyon", "trial_feature_contact_segment_management": "İletişim ve Segment Yönetimi", + "trial_feature_dashboards": "İçgörü Panoları", "trial_feature_email_followups": "E-posta Takipleri", + "trial_feature_feedback_record_directories": "Geri Bildirim Kayıt Dizinleri", "trial_feature_hide_branding": "Formbricks Markasını Gizle", "trial_feature_mobile_sdks": "iOS ve Android SDK'ları", "trial_feature_respondent_identification": "Katılımcı Kimlik Tespiti", + "trial_feature_unify_feedback": "Geri Bildirimi Birleştir Gelen Kutusu", "trial_feature_unlimited_seats": "Sınırsız Kullanıcı", "trial_feature_webhooks": "Özel Webhook'lar", "trial_no_credit_card": "14 günlük deneme, kredi kartı gerekmez", @@ -2572,6 +2577,8 @@ "title": "Geri Bildirim Kayıt Dizinleri", "unarchive": "Arşivden çıkar", "unarchive_workspace_conflict": "Atanmış çalışma alanlarından biri veya daha fazlası arşivlendiği için bu dizin arşivden çıkarılamaz.", + "upgrade_prompt_description": "Geri bildirim kayıtlarını dizinler halinde düzenleyin ve verileri doğru çalışma alanına yönlendirin. Pro ve Scale planlarında kullanılabilir.", + "upgrade_prompt_title": "Geri Bildirim Kayıt Dizinlerinin Kilidini Açmak İçin Yükseltin", "workspace_access": "Çalışma alanı erişimi" }, "general": { @@ -3773,6 +3780,8 @@ "unify_feedback": "Geri Bildirimleri Birleştir", "update_mapping_description": "Bu kaynak için eşleme yapılandırmasını güncelle.", "updated_at": "Güncellenme tarihi", + "upgrade_prompt_description": "Her kaynaktan gelen müşteri geri bildirimlerini tek bir gelen kutusunda birleştirin. Pro ve Scale planlarında kullanılabilir.", + "upgrade_prompt_title": "Geri Bildirimi Birleştir'in Kilidini Açmak İçin Yükseltin", "upload_csv_data_description": "Geri bildirim verilerini içe aktarmak için bir CSV dosyası yükle.", "upload_csv_file": "CSV Dosyası Yükle", "user_identifier": "Kullanıcı", diff --git a/apps/web/locales/zh-Hans-CN.json b/apps/web/locales/zh-Hans-CN.json index 1f0aedaa05..9d9ef959dc 100644 --- a/apps/web/locales/zh-Hans-CN.json +++ b/apps/web/locales/zh-Hans-CN.json @@ -1833,7 +1833,9 @@ "failed_to_load_chart_data": "加载图表数据失败", "no_dashboards_found": "未找到 Dashboard。", "no_data_message": "暂无数据。当前没有可显示的信息。请添加图表来构建你的仪表板。", - "please_enter_name": "请输入 Dashboard 名称" + "please_enter_name": "请输入 Dashboard 名称", + "upgrade_prompt_description": "从反馈数据构建洞察仪表板。专业版和规模版方案可用。", + "upgrade_prompt_title": "升级以解锁仪表板" }, "manage_feedback_sources": "Manage feedback sources", "no_feedback_records_message": "您没有可供报告的反馈记录。设置反馈源以将数据输入系统。", @@ -2448,10 +2450,13 @@ "trial_feature_api_access": "API 访问", "trial_feature_attribute_segmentation": "基于属性的细分", "trial_feature_contact_segment_management": "联系人和细分管理", + "trial_feature_dashboards": "洞察仪表板", "trial_feature_email_followups": "电子邮件跟进", + "trial_feature_feedback_record_directories": "反馈记录目录", "trial_feature_hide_branding": "隐藏 Formbricks 品牌标识", "trial_feature_mobile_sdks": "iOS 和 Android SDK", "trial_feature_respondent_identification": "受访者识别", + "trial_feature_unify_feedback": "统一反馈收件箱", "trial_feature_unlimited_seats": "无限席位", "trial_feature_webhooks": "自定义 Webhook", "trial_no_credit_card": "14 天试用,无需信用卡", @@ -2572,6 +2577,8 @@ "title": "反馈记录目录", "unarchive": "取消归档", "unarchive_workspace_conflict": "无法取消归档该目录,因为一个或多个已分配工作区已归档。", + "upgrade_prompt_description": "将反馈记录整理到目录中,并将数据路由到正确的工作空间。专业版和规模版方案可用。", + "upgrade_prompt_title": "升级以解锁反馈记录目录", "workspace_access": "工作区访问权限" }, "general": { @@ -3773,6 +3780,8 @@ "unify_feedback": "统一反馈", "update_mapping_description": "更新此来源的映射配置。", "updated_at": "更新于", + "upgrade_prompt_description": "在一个收件箱中统一来自各个来源的客户反馈。专业版和规模版方案可用。", + "upgrade_prompt_title": "升级以解锁统一反馈", "upload_csv_data_description": "上传 CSV 文件以导入反馈数据。", "upload_csv_file": "上传 CSV 文件", "user_identifier": "用户", diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json index 92a79d1ac8..da8623191d 100644 --- a/apps/web/locales/zh-Hant-TW.json +++ b/apps/web/locales/zh-Hant-TW.json @@ -1833,7 +1833,9 @@ "failed_to_load_chart_data": "載入圖表資料失敗", "no_dashboards_found": "找不到儀表板。", "no_data_message": "無資料。目前沒有可顯示的資訊。請新增圖表來建立你的儀表板。", - "please_enter_name": "請輸入儀表板名稱" + "please_enter_name": "請輸入儀表板名稱", + "upgrade_prompt_description": "從您的回饋資料建立洞察儀表板。專業版和企業版方案提供此功能。", + "upgrade_prompt_title": "升級以解鎖儀表板功能" }, "manage_feedback_sources": "Manage feedback sources", "no_feedback_records_message": "您沒有可供報告的回饋記錄。設定回饋來源以將資料輸入系統。", @@ -2448,10 +2450,13 @@ "trial_feature_api_access": "API 存取", "trial_feature_attribute_segmentation": "基於屬性的分群", "trial_feature_contact_segment_management": "聯絡人與分群管理", + "trial_feature_dashboards": "洞察儀表板", "trial_feature_email_followups": "電子郵件追蹤", + "trial_feature_feedback_record_directories": "回饋記錄目錄", "trial_feature_hide_branding": "隱藏 Formbricks 品牌標識", "trial_feature_mobile_sdks": "iOS 與 Android SDK", "trial_feature_respondent_identification": "受訪者識別", + "trial_feature_unify_feedback": "統一回饋收件匣", "trial_feature_unlimited_seats": "無限座位數", "trial_feature_webhooks": "自訂 Webhook", "trial_no_credit_card": "14 天試用,無需信用卡", @@ -2572,6 +2577,8 @@ "title": "意見回饋記錄目錄", "unarchive": "取消封存", "unarchive_workspace_conflict": "無法取消封存此目錄,因為一個或多個已指派工作區已封存。", + "upgrade_prompt_description": "將回饋記錄整理至目錄中,並將資料導向正確的工作區。專業版和企業版方案提供此功能。", + "upgrade_prompt_title": "升級以解鎖回饋記錄目錄功能", "workspace_access": "工作區存取權限" }, "general": { @@ -3773,6 +3780,8 @@ "unify_feedback": "整合回饋", "update_mapping_description": "更新此來源的對應設定。", "updated_at": "更新時間", + "upgrade_prompt_description": "將來自各個來源的顧客回饋統一在單一收件匣中。專業版和企業版方案提供此功能。", + "upgrade_prompt_title": "升級以解鎖統一回饋功能", "upload_csv_data_description": "上傳 CSV 檔案以匯入回饋資料。", "upload_csv_file": "上傳 CSV 檔案", "user_identifier": "使用者", diff --git a/apps/web/modules/billing/lib/stripe-catalog.ts b/apps/web/modules/billing/lib/stripe-catalog.ts index f29977ef8e..93ff90ac33 100644 --- a/apps/web/modules/billing/lib/stripe-catalog.ts +++ b/apps/web/modules/billing/lib/stripe-catalog.ts @@ -11,4 +11,7 @@ export const CLOUD_STRIPE_FEATURE_LOOKUP_KEYS = { CONTACTS: "contacts", AI_SMART_TOOLS: "ai-smart-tools", AI_DATA_ANALYSIS: "ai-data-analysis", + UNIFY_FEEDBACK: "unify-feedback", + FEEDBACK_RECORD_DIRECTORIES: "feedback-record-directories", + DASHBOARDS: "dashboards", } as const; diff --git a/apps/web/modules/ee/analysis/dashboards/actions.ts b/apps/web/modules/ee/analysis/dashboards/actions.ts index 06eea3c512..fd9864a34b 100644 --- a/apps/web/modules/ee/analysis/dashboards/actions.ts +++ b/apps/web/modules/ee/analysis/dashboards/actions.ts @@ -4,10 +4,12 @@ import { revalidatePath } from "next/cache"; import { z } from "zod"; import { ZWidgetLayout } from "@formbricks/types/analysis"; import { ZId } from "@formbricks/types/common"; +import { OperationNotAllowedError } from "@formbricks/types/errors"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context"; import { checkWorkspaceAccess } from "@/modules/ee/analysis/lib/access"; import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler"; +import { getIsDashboardsEnabled } from "@/modules/ee/license-check/lib/utils"; import { ZDashboardUpdateInput } from "../types/analysis"; import { addChartToDashboard, @@ -20,6 +22,13 @@ import { updateWidgetLayouts, } from "./lib/dashboards"; +const checkDashboardsEnabled = async (organizationId: string) => { + const isAllowed = await getIsDashboardsEnabled(organizationId); + if (!isAllowed) { + throw new OperationNotAllowedError("Dashboards are not enabled for this organization"); + } +}; + const ZCreateDashboardAction = z.object({ workspaceId: ZId, name: z.string().min(1), @@ -41,6 +50,7 @@ export const createDashboardAction = authenticatedActionClient.inputSchema(ZCrea parsedInput.workspaceId, "readWrite" ); + await checkDashboardsEnabled(organizationId); const dashboard = await createDashboard({ workspaceId, @@ -82,6 +92,7 @@ export const updateDashboardAction = authenticatedActionClient.inputSchema(ZUpda parsedInput.workspaceId, "readWrite" ); + await checkDashboardsEnabled(organizationId); const { dashboard, updatedDashboard } = await updateDashboard(parsedInput.dashboardId, workspaceId, { name: parsedInput.name, @@ -129,6 +140,7 @@ export const updateWidgetLayoutsAction = authenticatedActionClient parsedInput.workspaceId, "readWrite" ); + await checkDashboardsEnabled(organizationId); const dashboard = await getDashboard(parsedInput.dashboardId, workspaceId); @@ -167,6 +179,7 @@ export const deleteDashboardAction = authenticatedActionClient.inputSchema(ZDele parsedInput.workspaceId, "readWrite" ); + await checkDashboardsEnabled(organizationId); const dashboard = await deleteDashboard(parsedInput.dashboardId, workspaceId); @@ -204,6 +217,7 @@ export const duplicateDashboardAction = authenticatedActionClient parsedInput.workspaceId, "readWrite" ); + await checkDashboardsEnabled(organizationId); const dashboard = await duplicateDashboard(parsedInput.dashboardId, workspaceId, ctx.user.id); @@ -232,7 +246,12 @@ export const getDashboardsAction = authenticatedActionClient ctx: AuthenticatedActionClientCtx; parsedInput: z.infer; }) => { - const { workspaceId } = await checkWorkspaceAccess(ctx.user.id, parsedInput.workspaceId, "read"); + const { organizationId, workspaceId } = await checkWorkspaceAccess( + ctx.user.id, + parsedInput.workspaceId, + "read" + ); + await checkDashboardsEnabled(organizationId); return getDashboards(workspaceId); } @@ -253,7 +272,12 @@ export const getDashboardAction = authenticatedActionClient ctx: AuthenticatedActionClientCtx; parsedInput: z.infer; }) => { - const { workspaceId } = await checkWorkspaceAccess(ctx.user.id, parsedInput.workspaceId, "read"); + const { organizationId, workspaceId } = await checkWorkspaceAccess( + ctx.user.id, + parsedInput.workspaceId, + "read" + ); + await checkDashboardsEnabled(organizationId); return getDashboard(parsedInput.dashboardId, workspaceId); } @@ -284,6 +308,7 @@ export const addChartToDashboardAction = authenticatedActionClient parsedInput.workspaceId, "readWrite" ); + await checkDashboardsEnabled(organizationId); const widget = await addChartToDashboard({ dashboardId: parsedInput.dashboardId, diff --git a/apps/web/modules/ee/analysis/dashboards/pages/dashboard-detail-page.tsx b/apps/web/modules/ee/analysis/dashboards/pages/dashboard-detail-page.tsx index d87474bb5d..2a923e5745 100644 --- a/apps/web/modules/ee/analysis/dashboards/pages/dashboard-detail-page.tsx +++ b/apps/web/modules/ee/analysis/dashboards/pages/dashboard-detail-page.tsx @@ -2,10 +2,15 @@ import { notFound } from "next/navigation"; import { logger } from "@formbricks/logger"; import type { TChartQuery } from "@formbricks/types/analysis"; import { ResourceNotFoundError } from "@formbricks/types/errors"; +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; +import { getTranslate } from "@/lingodotdev/server"; import { executeQuery } from "@/modules/ee/analysis/api/lib/cube-client"; import { injectTenantFilter } from "@/modules/ee/analysis/charts/lib/chart-utils"; +import { AnalysisPageLayout } from "@/modules/ee/analysis/components/analysis-page-layout"; import type { TChartDataRow } from "@/modules/ee/analysis/types/analysis"; import { getFeedbackRecordDirectoriesByWorkspaceId } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory"; +import { getIsDashboardsEnabled } from "@/modules/ee/license-check/lib/utils"; +import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt"; import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils"; import { DashboardDetailClient } from "../components/dashboard-detail-client"; import { getDashboard } from "../lib/dashboards"; @@ -37,8 +42,37 @@ export async function DashboardDetailPage({ }: Readonly<{ params: Promise<{ workspaceId: string; dashboardId: string }>; }>) { + const t = await getTranslate(); const { workspaceId, dashboardId } = await params; - const { isReadOnly } = await getWorkspaceAuth(workspaceId); + const { isReadOnly, organization } = await getWorkspaceAuth(workspaceId); + + const isDashboardsAllowed = await getIsDashboardsEnabled(organization.id); + if (!isDashboardsAllowed) { + return ( + + + + ); + } + const directories = await getFeedbackRecordDirectoriesByWorkspaceId(workspaceId); let dashboard; diff --git a/apps/web/modules/ee/analysis/dashboards/pages/dashboards-list-page.tsx b/apps/web/modules/ee/analysis/dashboards/pages/dashboards-list-page.tsx index b4bd3ec90c..3990009006 100644 --- a/apps/web/modules/ee/analysis/dashboards/pages/dashboards-list-page.tsx +++ b/apps/web/modules/ee/analysis/dashboards/pages/dashboards-list-page.tsx @@ -1,9 +1,12 @@ import { use } from "react"; import { getConnectorsWithMappings } from "@/lib/connector/service"; +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; import { getTranslate } from "@/lingodotdev/server"; import { AnalysisPageLayout } from "@/modules/ee/analysis/components/analysis-page-layout"; import { NoFeedbackRecordsState } from "@/modules/ee/analysis/components/no-feedback-records-state"; import { hasWorkspaceFeedbackRecords } from "@/modules/ee/analysis/lib/feedback-records"; +import { getIsDashboardsEnabled } from "@/modules/ee/license-check/lib/utils"; +import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt"; import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils"; import { TDashboardWithCount } from "../../types/analysis"; import { CreateDashboardButton } from "../components/create-dashboard-button"; @@ -32,7 +35,34 @@ interface DashboardsListPageProps { export const DashboardsListPage = async ({ workspaceId }: Readonly) => { const t = await getTranslate(); - const { isReadOnly } = await getWorkspaceAuth(workspaceId); + const { isReadOnly, organization } = await getWorkspaceAuth(workspaceId); + + const isDashboardsAllowed = await getIsDashboardsEnabled(organization.id); + if (!isDashboardsAllowed) { + return ( + + + + ); + } const [hasFeedbackRecords, connectors] = await Promise.all([ hasWorkspaceFeedbackRecords(workspaceId), diff --git a/apps/web/modules/ee/billing/components/select-plan-card.tsx b/apps/web/modules/ee/billing/components/select-plan-card.tsx index 23990fc587..5d8f48931e 100644 --- a/apps/web/modules/ee/billing/components/select-plan-card.tsx +++ b/apps/web/modules/ee/billing/components/select-plan-card.tsx @@ -45,6 +45,9 @@ export const SelectPlanCard = ({ nextUrl, organizationId }: SelectPlanCardProps) t("workspace.settings.billing.trial_feature_email_followups"), t("workspace.settings.billing.trial_feature_webhooks"), t("workspace.settings.billing.trial_feature_api_access"), + t("workspace.settings.billing.trial_feature_unify_feedback"), + t("workspace.settings.billing.trial_feature_feedback_record_directories"), + t("workspace.settings.billing.trial_feature_dashboards"), ] as const; const handleStartTrial = async () => { diff --git a/apps/web/modules/ee/feedback-record-directory/actions.ts b/apps/web/modules/ee/feedback-record-directory/actions.ts index c676f3f14c..b712e1c6d8 100644 --- a/apps/web/modules/ee/feedback-record-directory/actions.ts +++ b/apps/web/modules/ee/feedback-record-directory/actions.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { ZId } from "@formbricks/types/common"; +import { OperationNotAllowedError } from "@formbricks/types/errors"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware"; import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler"; @@ -12,6 +13,14 @@ import { updateFeedbackRecordDirectory, } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory"; import { ZFeedbackRecordDirectoryUpdateInput } from "@/modules/ee/feedback-record-directory/types/feedback-record-directory"; +import { getIsFeedbackRecordDirectoriesEnabled } from "@/modules/ee/license-check/lib/utils"; + +const checkFeedbackRecordDirectoriesEnabled = async (organizationId: string) => { + const isAllowed = await getIsFeedbackRecordDirectoriesEnabled(organizationId); + if (!isAllowed) { + throw new OperationNotAllowedError("Feedback Record Directories are not enabled for this organization"); + } +}; const ZCreateFeedbackRecordDirectoryAction = z.object({ organizationId: ZId, @@ -23,6 +32,7 @@ export const createFeedbackRecordDirectoryAction = authenticatedActionClient .inputSchema(ZCreateFeedbackRecordDirectoryAction) .action( withAuditLogging("created", "feedbackRecordDirectory", async ({ ctx, parsedInput }) => { + await checkFeedbackRecordDirectoriesEnabled(parsedInput.organizationId); await checkAuthorizationUpdated({ userId: ctx.user.id, organizationId: parsedInput.organizationId, @@ -56,6 +66,7 @@ export const getFeedbackRecordDirectoryDetailsAction = authenticatedActionClient .inputSchema(ZGetFeedbackRecordDirectoryDetailsAction) .action(async ({ parsedInput, ctx }) => { const organizationId = await getOrganizationIdFromDirectoryId(parsedInput.directoryId); + await checkFeedbackRecordDirectoriesEnabled(organizationId); await checkAuthorizationUpdated({ userId: ctx.user.id, @@ -82,6 +93,7 @@ export const updateFeedbackRecordDirectoryAction = authenticatedActionClient .action( withAuditLogging("updated", "feedbackRecordDirectory", async ({ ctx, parsedInput }) => { const organizationId = await getOrganizationIdFromDirectoryId(parsedInput.directoryId); + await checkFeedbackRecordDirectoriesEnabled(organizationId); await checkAuthorizationUpdated({ userId: ctx.user.id, diff --git a/apps/web/modules/ee/feedback-record-directory/page.tsx b/apps/web/modules/ee/feedback-record-directory/page.tsx index fa0194854f..1405c71c6d 100644 --- a/apps/web/modules/ee/feedback-record-directory/page.tsx +++ b/apps/web/modules/ee/feedback-record-directory/page.tsx @@ -3,8 +3,10 @@ import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; import { getAccessFlags } from "@/lib/membership/utils"; import { getTranslate } from "@/lingodotdev/server"; import { FeedbackRecordDirectoryView } from "@/modules/ee/feedback-record-directory/components/feedback-record-directory-view"; +import { getIsFeedbackRecordDirectoriesEnabled } from "@/modules/ee/license-check/lib/utils"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; +import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt"; import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils"; export const FeedbackRecordDirectoriesPage = async (props: { params: Promise<{ workspaceId: string }> }) => { @@ -15,6 +17,40 @@ export const FeedbackRecordDirectoriesPage = async (props: { params: Promise<{ w const { isOwner, isManager } = getAccessFlags(currentUserMembership.role); + const isFeedbackRecordDirectoriesAllowed = await getIsFeedbackRecordDirectoriesEnabled(organization.id); + if (!isFeedbackRecordDirectoriesAllowed) { + return ( + + + + + + + ); + } + if (!isOwner && !isManager) { return ( diff --git a/apps/web/modules/ee/license-check/lib/license.test.ts b/apps/web/modules/ee/license-check/lib/license.test.ts index 61e6711095..ccf0da6544 100644 --- a/apps/web/modules/ee/license-check/lib/license.test.ts +++ b/apps/web/modules/ee/license-check/lib/license.test.ts @@ -151,6 +151,9 @@ describe("License Core Logic", () => { auditLogs: true, accessControl: true, quotas: true, + unifyFeedback: false, + feedbackRecordDirectories: false, + dashboards: false, }; const mockFetchedLicenseDetails: TEnterpriseLicenseDetails = { status: "active", @@ -289,6 +292,9 @@ describe("License Core Logic", () => { auditLogs: false, accessControl: false, quotas: false, + unifyFeedback: false, + feedbackRecordDirectories: false, + dashboards: false, }, lastChecked: expect.any(Date), }, @@ -311,6 +317,9 @@ describe("License Core Logic", () => { auditLogs: false, accessControl: false, quotas: false, + unifyFeedback: false, + feedbackRecordDirectories: false, + dashboards: false, }, lastChecked: expect.any(Date), isPendingDowngrade: false, @@ -342,6 +351,9 @@ describe("License Core Logic", () => { auditLogs: false, accessControl: false, quotas: false, + unifyFeedback: false, + feedbackRecordDirectories: false, + dashboards: false, }; expect(mockCache.set).toHaveBeenCalledWith( expect.stringContaining("fb:license:"), @@ -530,6 +542,9 @@ describe("License Core Logic", () => { auditLogs: true, accessControl: true, quotas: true, + unifyFeedback: false, + feedbackRecordDirectories: false, + dashboards: false, }, }; @@ -595,6 +610,9 @@ describe("License Core Logic", () => { auditLogs: true, accessControl: true, quotas: true, + unifyFeedback: false, + feedbackRecordDirectories: false, + dashboards: false, }, }; @@ -651,6 +669,9 @@ describe("License Core Logic", () => { auditLogs: true, accessControl: true, quotas: true, + unifyFeedback: false, + feedbackRecordDirectories: false, + dashboards: false, }, }; @@ -855,6 +876,9 @@ describe("License Core Logic", () => { auditLogs: false, accessControl: false, quotas: false, + unifyFeedback: false, + feedbackRecordDirectories: false, + dashboards: false, }, }, }, @@ -1037,6 +1061,9 @@ describe("License Core Logic", () => { auditLogs: true, accessControl: true, quotas: true, + unifyFeedback: false, + feedbackRecordDirectories: false, + dashboards: false, }, }; @@ -1164,6 +1191,9 @@ describe("License Core Logic", () => { auditLogs: true, accessControl: true, quotas: true, + unifyFeedback: false, + feedbackRecordDirectories: false, + dashboards: false, }, }; diff --git a/apps/web/modules/ee/license-check/lib/license.ts b/apps/web/modules/ee/license-check/lib/license.ts index 74a424b6b5..09d04a782a 100644 --- a/apps/web/modules/ee/license-check/lib/license.ts +++ b/apps/web/modules/ee/license-check/lib/license.ts @@ -84,6 +84,9 @@ const LicenseFeaturesSchema = z.object({ auditLogs: z.boolean(), accessControl: z.boolean(), quotas: z.boolean(), + unifyFeedback: z.boolean().default(false), + feedbackRecordDirectories: z.boolean().default(false), + dashboards: z.boolean().default(false), }); const LicenseDetailsSchema = z.object({ @@ -152,6 +155,9 @@ const DEFAULT_FEATURES: TEnterpriseLicenseFeatures = { auditLogs: false, accessControl: false, quotas: false, + unifyFeedback: false, + feedbackRecordDirectories: false, + dashboards: false, }; // Helper functions diff --git a/apps/web/modules/ee/license-check/lib/utils.test.ts b/apps/web/modules/ee/license-check/lib/utils.test.ts index 02dd9c4ee4..6b43bfd170 100644 --- a/apps/web/modules/ee/license-check/lib/utils.test.ts +++ b/apps/web/modules/ee/license-check/lib/utils.test.ts @@ -13,12 +13,15 @@ import { getIsAISmartToolsEnabled, getIsAuditLogsEnabled, getIsContactsEnabled, + getIsDashboardsEnabled, + getIsFeedbackRecordDirectoriesEnabled, getIsMultiOrgEnabled, getIsQuotasEnabled, getIsSamlSsoEnabled, getIsSpamProtectionEnabled, getIsSsoEnabled, getIsTwoFactorAuthEnabled, + getIsUnifyFeedbackEnabled, getOrganizationWorkspacesLimit, getRemoveBrandingPermission, getWhiteLabelPermission, @@ -62,6 +65,9 @@ const defaultFeatures: TEnterpriseLicenseFeatures = { auditLogs: false, accessControl: false, quotas: false, + unifyFeedback: false, + feedbackRecordDirectories: false, + dashboards: false, }; const defaultLicense = { @@ -264,6 +270,86 @@ describe("License Utils", () => { expect(smartTools).toBe(false); expect(dataAnalysis).toBe(false); }); + + test("uses cloud unify feedback entitlement", async () => { + vi.mocked(constants).IS_FORMBRICKS_CLOUD = true; + vi.mocked(hasOrganizationEntitlementWithLicenseGuard).mockResolvedValueOnce(true); + + const result = await getIsUnifyFeedbackEnabled("org_1"); + + expect(result).toBe(true); + expect(hasOrganizationEntitlementWithLicenseGuard).toHaveBeenCalledWith( + "org_1", + CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.UNIFY_FEEDBACK + ); + }); + + test("uses cloud feedback record directories entitlement", async () => { + vi.mocked(constants).IS_FORMBRICKS_CLOUD = true; + vi.mocked(hasOrganizationEntitlementWithLicenseGuard).mockResolvedValueOnce(true); + + const result = await getIsFeedbackRecordDirectoriesEnabled("org_1"); + + expect(result).toBe(true); + expect(hasOrganizationEntitlementWithLicenseGuard).toHaveBeenCalledWith( + "org_1", + CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.FEEDBACK_RECORD_DIRECTORIES + ); + }); + + test("uses cloud dashboards entitlement", async () => { + vi.mocked(constants).IS_FORMBRICKS_CLOUD = true; + vi.mocked(hasOrganizationEntitlementWithLicenseGuard).mockResolvedValueOnce(true); + + const result = await getIsDashboardsEnabled("org_1"); + + expect(result).toBe(true); + expect(hasOrganizationEntitlementWithLicenseGuard).toHaveBeenCalledWith( + "org_1", + CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.DASHBOARDS + ); + }); + + test("returns self-hosted unify feedback / FRD / dashboards from license", async () => { + vi.mocked(constants).IS_FORMBRICKS_CLOUD = false; + vi.mocked(getEnterpriseLicense).mockResolvedValue({ + ...defaultLicense, + features: { + ...defaultFeatures, + unifyFeedback: true, + feedbackRecordDirectories: true, + dashboards: true, + }, + }); + + const [unify, frd, dashboards] = await Promise.all([ + getIsUnifyFeedbackEnabled("org_1"), + getIsFeedbackRecordDirectoriesEnabled("org_1"), + getIsDashboardsEnabled("org_1"), + ]); + + expect(unify).toBe(true); + expect(frd).toBe(true); + expect(dashboards).toBe(true); + }); + + test("returns false for self-hosted unify feedback / FRD / dashboards when not enabled", async () => { + vi.mocked(constants).IS_FORMBRICKS_CLOUD = false; + vi.mocked(getEnterpriseLicense).mockResolvedValue({ + ...defaultLicense, + features: defaultFeatures, + }); + + const [unify, frd, dashboards] = await Promise.all([ + getIsUnifyFeedbackEnabled("org_1"), + getIsFeedbackRecordDirectoriesEnabled("org_1"), + getIsDashboardsEnabled("org_1"), + ]); + + expect(unify).toBe(false); + expect(frd).toBe(false); + expect(dashboards).toBe(false); + }); }); describe("getBiggerUploadFileSizePermission", () => { diff --git a/apps/web/modules/ee/license-check/lib/utils.ts b/apps/web/modules/ee/license-check/lib/utils.ts index ea6661449a..f401f7ee58 100644 --- a/apps/web/modules/ee/license-check/lib/utils.ts +++ b/apps/web/modules/ee/license-check/lib/utils.ts @@ -31,7 +31,14 @@ const getCustomPlanFeaturePermission = async ( organizationId: string, featureKey: keyof Pick< TEnterpriseLicenseFeatures, - "accessControl" | "quotas" | "contacts" | "aiSmartTools" | "aiDataAnalysis" + | "accessControl" + | "quotas" + | "contacts" + | "aiSmartTools" + | "aiDataAnalysis" + | "unifyFeedback" + | "feedbackRecordDirectories" + | "dashboards" > ): Promise => { if (IS_FORMBRICKS_CLOUD) { @@ -41,6 +48,9 @@ const getCustomPlanFeaturePermission = async ( contacts: CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.CONTACTS, aiSmartTools: CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.AI_SMART_TOOLS, aiDataAnalysis: CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.AI_DATA_ANALYSIS, + unifyFeedback: CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.UNIFY_FEEDBACK, + feedbackRecordDirectories: CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.FEEDBACK_RECORD_DIRECTORIES, + dashboards: CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.DASHBOARDS, }; const lookupKey = featureLookupKeyMap[featureKey]; if (lookupKey) { @@ -154,6 +164,18 @@ export const getAccessControlPermission = async (organizationId: string): Promis return getCustomPlanFeaturePermission(organizationId, "accessControl"); }; +export const getIsUnifyFeedbackEnabled = async (organizationId: string): Promise => { + return getCustomPlanFeaturePermission(organizationId, "unifyFeedback"); +}; + +export const getIsFeedbackRecordDirectoriesEnabled = async (organizationId: string): Promise => { + return getCustomPlanFeaturePermission(organizationId, "feedbackRecordDirectories"); +}; + +export const getIsDashboardsEnabled = async (organizationId: string): Promise => { + return getCustomPlanFeaturePermission(organizationId, "dashboards"); +}; + export const getOrganizationWorkspacesLimit = async (organizationId: string): Promise => { const entitlementsContext = await getOrganizationEntitlementsContext(organizationId); diff --git a/apps/web/modules/ee/license-check/types/enterprise-license.ts b/apps/web/modules/ee/license-check/types/enterprise-license.ts index 2e85355dcb..4f418f9394 100644 --- a/apps/web/modules/ee/license-check/types/enterprise-license.ts +++ b/apps/web/modules/ee/license-check/types/enterprise-license.ts @@ -19,6 +19,9 @@ const ZEnterpriseLicenseFeatures = z.object({ auditLogs: z.boolean(), accessControl: z.boolean(), quotas: z.boolean(), + unifyFeedback: z.boolean().default(false), + feedbackRecordDirectories: z.boolean().default(false), + dashboards: z.boolean().default(false), }); export type TEnterpriseLicenseFeatures = z.infer; diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/actions.ts b/apps/web/modules/ee/unify-feedback/actions.ts similarity index 94% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/actions.ts rename to apps/web/modules/ee/unify-feedback/actions.ts index c3d84cf783..dc38e2df9f 100644 --- a/apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/actions.ts +++ b/apps/web/modules/ee/unify-feedback/actions.ts @@ -1,10 +1,12 @@ "use server"; +import { OperationNotAllowedError } from "@formbricks/types/errors"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware"; import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context"; import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper"; import { getFeedbackRecordDirectoriesByWorkspaceId } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory"; +import { getIsUnifyFeedbackEnabled } from "@/modules/ee/license-check/lib/utils"; import { createFeedbackRecord, retrieveFeedbackRecord, updateFeedbackRecord } from "@/modules/hub/service"; import type { FeedbackRecordCreateParams, FeedbackRecordUpdateParams } from "@/modules/hub/types"; import { @@ -22,6 +24,10 @@ const ensureAccess = async ( minPermission: "read" | "readWrite" ): Promise => { const organizationId = await getOrganizationIdFromWorkspaceId(workspaceId); + const isUnifyFeedbackAllowed = await getIsUnifyFeedbackEnabled(organizationId); + if (!isUnifyFeedbackAllowed) { + throw new OperationNotAllowedError("Unify Feedback is not enabled for this organization"); + } await checkAuthorizationUpdated({ userId, organizationId, diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/components/feedback-record-form-drawer.tsx b/apps/web/modules/ee/unify-feedback/components/feedback-record-form-drawer.tsx similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/components/feedback-record-form-drawer.tsx rename to apps/web/modules/ee/unify-feedback/components/feedback-record-form-drawer.tsx diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/components/feedback-records-page-client.tsx b/apps/web/modules/ee/unify-feedback/components/feedback-records-page-client.tsx similarity index 93% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/components/feedback-records-page-client.tsx rename to apps/web/modules/ee/unify-feedback/components/feedback-records-page-client.tsx index 55f6bc8e9a..66c9f69d70 100644 --- a/apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/components/feedback-records-page-client.tsx +++ b/apps/web/modules/ee/unify-feedback/components/feedback-records-page-client.tsx @@ -4,8 +4,8 @@ import { useTranslation } from "react-i18next"; import type { FeedbackRecordData } from "@/modules/hub/types"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; -import { UnifyConfigNavigation } from "../../components/UnifyConfigNavigation"; import { FeedbackRecordsTable } from "./feedback-records-table"; +import { UnifyConfigNavigation } from "./unify-config-navigation"; interface FeedbackRecordsPageClientProps { workspaceId: string; diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/components/feedback-records-table.tsx b/apps/web/modules/ee/unify-feedback/components/feedback-records-table.tsx similarity index 99% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/components/feedback-records-table.tsx rename to apps/web/modules/ee/unify-feedback/components/feedback-records-table.tsx index b919398eb1..bb09e52449 100644 --- a/apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/components/feedback-records-table.tsx +++ b/apps/web/modules/ee/unify-feedback/components/feedback-records-table.tsx @@ -29,8 +29,8 @@ import { DropdownMenuTrigger, } from "@/modules/ui/components/dropdown-menu"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip"; -import { CsvImportModal } from "../../sources/components/csv-import-modal"; import { formatSourceType } from "../lib/utils"; +import { CsvImportModal } from "../sources/components/csv-import-modal"; import { FeedbackRecordFormDrawer } from "./feedback-record-form-drawer"; const RECORDS_PER_PAGE = 50; diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/components/UnifyConfigNavigation.tsx b/apps/web/modules/ee/unify-feedback/components/unify-config-navigation.tsx similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/components/UnifyConfigNavigation.tsx rename to apps/web/modules/ee/unify-feedback/components/unify-config-navigation.tsx diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/lib/types.ts b/apps/web/modules/ee/unify-feedback/lib/types.ts similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/lib/types.ts rename to apps/web/modules/ee/unify-feedback/lib/types.ts diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/lib/utils.test.ts b/apps/web/modules/ee/unify-feedback/lib/utils.test.ts similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/lib/utils.test.ts rename to apps/web/modules/ee/unify-feedback/lib/utils.test.ts diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/lib/utils.ts b/apps/web/modules/ee/unify-feedback/lib/utils.ts similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/lib/utils.ts rename to apps/web/modules/ee/unify-feedback/lib/utils.ts diff --git a/apps/web/modules/ee/unify-feedback/page.tsx b/apps/web/modules/ee/unify-feedback/page.tsx new file mode 100644 index 0000000000..dfd4e56d96 --- /dev/null +++ b/apps/web/modules/ee/unify-feedback/page.tsx @@ -0,0 +1,92 @@ +import { notFound } from "next/navigation"; +import { getConnectorsWithMappings } from "@/lib/connector/service"; +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; +import { getTranslate } from "@/lingodotdev/server"; +import { getFeedbackRecordDirectoriesByWorkspaceId } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory"; +import { getIsUnifyFeedbackEnabled } from "@/modules/ee/license-check/lib/utils"; +import { listFeedbackRecords } from "@/modules/hub/service"; +import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; +import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt"; +import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils"; +import { FeedbackRecordsPageClient } from "./components/feedback-records-page-client"; + +const INITIAL_PAGE_SIZE = 50; + +export const UnifyFeedbackRecordsPage = async ( + props: Readonly<{ params: Promise<{ workspaceId: string }> }> +) => { + const t = await getTranslate(); + const params = await props.params; + + const { isOwner, isManager, hasReadAccess, hasReadWriteAccess, hasManageAccess, session, organization } = + await getWorkspaceAuth(params.workspaceId); + + if (!session) { + throw new Error(t("common.session_not_found")); + } + + const hasAccess = isOwner || isManager || hasReadAccess || hasReadWriteAccess || hasManageAccess; + const canWrite = isOwner || isManager || hasReadWriteAccess || hasManageAccess; + if (!hasAccess) { + return notFound(); + } + + const isUnifyFeedbackAllowed = await getIsUnifyFeedbackEnabled(organization.id); + if (!isUnifyFeedbackAllowed) { + return ( + + + + ); + } + + const [frds, connectors] = await Promise.all([ + getFeedbackRecordDirectoriesByWorkspaceId(params.workspaceId), + getConnectorsWithMappings(params.workspaceId), + ]); + + const results = await Promise.all( + frds.map((frd) => listFeedbackRecords({ tenant_id: frd.id, limit: INITIAL_PAGE_SIZE })) + ); + + // Don't crash if Hub is unreachable — show empty state + const successfulResults = results.filter((r) => !r.error); + + const merged = successfulResults + .flatMap((r) => r.data?.data ?? []) + .toSorted((a, b) => (a.collected_at < b.collected_at ? 1 : -1)) + .slice(0, INITIAL_PAGE_SIZE); + + const frdMap = Object.fromEntries(frds.map((f) => [f.id, f.name])); + const csvSources = connectors + .filter((connector) => connector.type === "csv") + .map((connector) => ({ id: connector.id, name: connector.name })); + + return ( + + ); +}; diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/actions.ts b/apps/web/modules/ee/unify-feedback/sources/actions.ts similarity index 77% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/actions.ts rename to apps/web/modules/ee/unify-feedback/sources/actions.ts index 895a9f2993..4c4d57f647 100644 --- a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/actions.ts +++ b/apps/web/modules/ee/unify-feedback/sources/actions.ts @@ -2,10 +2,12 @@ import { z } from "zod"; import { ZId } from "@formbricks/types/common"; +import { OperationNotAllowedError } from "@formbricks/types/errors"; import { getSurveys } from "@/lib/survey/service"; import { authenticatedActionClient } from "@/lib/utils/action-client"; import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware"; import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper"; +import { getIsUnifyFeedbackEnabled } from "@/modules/ee/license-check/lib/utils"; import { transformToUnifySurvey } from "./lib"; import { TUnifySurvey } from "./types"; @@ -17,6 +19,10 @@ export const getSurveysForUnifyAction = authenticatedActionClient .schema(ZGetSurveysForUnifyAction) .action(async ({ ctx, parsedInput }): Promise => { const organizationId = await getOrganizationIdFromWorkspaceId(parsedInput.workspaceId); + const isUnifyFeedbackAllowed = await getIsUnifyFeedbackEnabled(organizationId); + if (!isUnifyFeedbackAllowed) { + throw new OperationNotAllowedError("Unify Feedback is not enabled for this organization"); + } await checkAuthorizationUpdated({ userId: ctx.user.id, organizationId, diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/connector-display.tsx b/apps/web/modules/ee/unify-feedback/sources/components/connector-display.tsx similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/connector-display.tsx rename to apps/web/modules/ee/unify-feedback/sources/components/connector-display.tsx diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/connector-row-dropdown.tsx b/apps/web/modules/ee/unify-feedback/sources/components/connector-row-dropdown.tsx similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/connector-row-dropdown.tsx rename to apps/web/modules/ee/unify-feedback/sources/components/connector-row-dropdown.tsx diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/connector-type-selector.tsx b/apps/web/modules/ee/unify-feedback/sources/components/connector-type-selector.tsx similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/connector-type-selector.tsx rename to apps/web/modules/ee/unify-feedback/sources/components/connector-type-selector.tsx diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/connectors-page-client.tsx b/apps/web/modules/ee/unify-feedback/sources/components/connectors-page-client.tsx similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/connectors-page-client.tsx rename to apps/web/modules/ee/unify-feedback/sources/components/connectors-page-client.tsx diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/connectors-table-data-row.tsx b/apps/web/modules/ee/unify-feedback/sources/components/connectors-table-data-row.tsx similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/connectors-table-data-row.tsx rename to apps/web/modules/ee/unify-feedback/sources/components/connectors-table-data-row.tsx diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/connectors-table-rows-container.tsx b/apps/web/modules/ee/unify-feedback/sources/components/connectors-table-rows-container.tsx similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/connectors-table-rows-container.tsx rename to apps/web/modules/ee/unify-feedback/sources/components/connectors-table-rows-container.tsx diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/connectors-table.tsx b/apps/web/modules/ee/unify-feedback/sources/components/connectors-table.tsx similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/connectors-table.tsx rename to apps/web/modules/ee/unify-feedback/sources/components/connectors-table.tsx diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/create-connector-modal.tsx b/apps/web/modules/ee/unify-feedback/sources/components/create-connector-modal.tsx similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/create-connector-modal.tsx rename to apps/web/modules/ee/unify-feedback/sources/components/create-connector-modal.tsx diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/csv-connector-ui.tsx b/apps/web/modules/ee/unify-feedback/sources/components/csv-connector-ui.tsx similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/csv-connector-ui.tsx rename to apps/web/modules/ee/unify-feedback/sources/components/csv-connector-ui.tsx diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/csv-import-modal.tsx b/apps/web/modules/ee/unify-feedback/sources/components/csv-import-modal.tsx similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/csv-import-modal.tsx rename to apps/web/modules/ee/unify-feedback/sources/components/csv-import-modal.tsx diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/edit-connector-modal.tsx b/apps/web/modules/ee/unify-feedback/sources/components/edit-connector-modal.tsx similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/edit-connector-modal.tsx rename to apps/web/modules/ee/unify-feedback/sources/components/edit-connector-modal.tsx diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/formbricks-question-list.tsx b/apps/web/modules/ee/unify-feedback/sources/components/formbricks-question-list.tsx similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/formbricks-question-list.tsx rename to apps/web/modules/ee/unify-feedback/sources/components/formbricks-question-list.tsx diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/mapping-field.tsx b/apps/web/modules/ee/unify-feedback/sources/components/mapping-field.tsx similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/mapping-field.tsx rename to apps/web/modules/ee/unify-feedback/sources/components/mapping-field.tsx diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/mapping-ui.tsx b/apps/web/modules/ee/unify-feedback/sources/components/mapping-ui.tsx similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/components/mapping-ui.tsx rename to apps/web/modules/ee/unify-feedback/sources/components/mapping-ui.tsx diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/lib.test.ts b/apps/web/modules/ee/unify-feedback/sources/lib.test.ts similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/lib.test.ts rename to apps/web/modules/ee/unify-feedback/sources/lib.test.ts diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/lib.ts b/apps/web/modules/ee/unify-feedback/sources/lib.ts similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/lib.ts rename to apps/web/modules/ee/unify-feedback/sources/lib.ts diff --git a/apps/web/modules/ee/unify-feedback/sources/page.tsx b/apps/web/modules/ee/unify-feedback/sources/page.tsx new file mode 100644 index 0000000000..b5464b9161 --- /dev/null +++ b/apps/web/modules/ee/unify-feedback/sources/page.tsx @@ -0,0 +1,75 @@ +import { notFound } from "next/navigation"; +import { getConnectorsWithMappings } from "@/lib/connector/service"; +import { IS_FORMBRICKS_CLOUD } from "@/lib/constants"; +import { getSurveys } from "@/lib/survey/service"; +import { getTranslate } from "@/lingodotdev/server"; +import { getFeedbackRecordDirectoriesByWorkspaceId } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory"; +import { getIsUnifyFeedbackEnabled } from "@/modules/ee/license-check/lib/utils"; +import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; +import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt"; +import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils"; +import { ConnectorsSection } from "./components/connectors-page-client"; +import { transformToUnifySurvey } from "./lib"; + +export const WorkspaceFeedbackSourcesPage = async ( + props: Readonly<{ params: Promise<{ workspaceId: string }> }> +) => { + const t = await getTranslate(); + const params = await props.params; + + const { isOwner, isManager, hasReadAccess, hasReadWriteAccess, hasManageAccess, session, organization } = + await getWorkspaceAuth(params.workspaceId); + + if (!session) { + throw new Error(t("common.session_not_found")); + } + + const hasAccess = isOwner || isManager || hasReadAccess || hasReadWriteAccess || hasManageAccess; + if (!hasAccess) { + return notFound(); + } + + const isUnifyFeedbackAllowed = await getIsUnifyFeedbackEnabled(organization.id); + if (!isUnifyFeedbackAllowed) { + return ( + + + + ); + } + + const [connectors, surveys, directories] = await Promise.all([ + getConnectorsWithMappings(params.workspaceId), + getSurveys(params.workspaceId), + getFeedbackRecordDirectoriesByWorkspaceId(params.workspaceId), + ]); + + const unifySurveys = surveys.map(transformToUnifySurvey); + + return ( + + ); +}; diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/types.ts b/apps/web/modules/ee/unify-feedback/sources/types.ts similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/types.ts rename to apps/web/modules/ee/unify-feedback/sources/types.ts diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/utils.test.ts b/apps/web/modules/ee/unify-feedback/sources/utils.test.ts similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/utils.test.ts rename to apps/web/modules/ee/unify-feedback/sources/utils.test.ts diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/utils.ts b/apps/web/modules/ee/unify-feedback/sources/utils.ts similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/sources/utils.ts rename to apps/web/modules/ee/unify-feedback/sources/utils.ts diff --git a/apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/types.ts b/apps/web/modules/ee/unify-feedback/types.ts similarity index 100% rename from apps/web/app/(app)/workspaces/[workspaceId]/unify/feedback-records/types.ts rename to apps/web/modules/ee/unify-feedback/types.ts diff --git a/apps/web/modules/entitlements/lib/checks.ts b/apps/web/modules/entitlements/lib/checks.ts index e197104442..67aad808c2 100644 --- a/apps/web/modules/entitlements/lib/checks.ts +++ b/apps/web/modules/entitlements/lib/checks.ts @@ -12,6 +12,9 @@ const LICENSE_GUARDED_ENTITLEMENTS: Partial { return null; }; -const parseFeedbackRecordsGatewayRoute = ( - method: string, - pathname: string -): TParsedGatewayRoute | null => { +const parseFeedbackRecordsGatewayRoute = (method: string, pathname: string): TParsedGatewayRoute | null => { const normalizedPath = normalizeFeedbackRecordsPath(pathname); if (!normalizedPath) { return null; @@ -220,7 +216,10 @@ const resolveTenantId = async ( const tenantLookup = await getFeedbackRecordTenant(route.recordId!); if (tenantLookup.error) { if (tenantLookup.error.status === 404) { - logger.warn({ requestId, recordId: route.recordId }, "Feedback record tenant lookup returned not found"); + logger.warn( + { requestId, recordId: route.recordId }, + "Feedback record tenant lookup returned not found" + ); return { errorResponse: buildStatusResponse(403, "Forbidden"), }; @@ -237,7 +236,10 @@ const resolveTenantId = async ( const tenantId = parseTenantId(tenantLookup.data?.tenantId ?? null); if (!tenantId) { - logger.warn({ requestId, recordId: route.recordId }, "Feedback record tenant lookup returned invalid tenant"); + logger.warn( + { requestId, recordId: route.recordId }, + "Feedback record tenant lookup returned invalid tenant" + ); return { errorResponse: buildStatusResponse(503, "Feedback record lookup failed"), }; @@ -256,6 +258,11 @@ const authorizeGatewayRequest = async ( return { allowed: false }; } + const isUnifyFeedbackAllowed = await getIsUnifyFeedbackEnabled(feedbackRecordDirectory.organizationId); + if (!isUnifyFeedbackAllowed) { + return { allowed: false }; + } + if (principal.type === "apiKey") { return hasFeedbackRecordDirectoryPermission( principal.authentication, @@ -267,8 +274,7 @@ const authorizeGatewayRequest = async ( } try { - const minPermission: "read" | "readWrite" = - requiredPermission === "read" ? "read" : "readWrite"; + const minPermission: "read" | "readWrite" = requiredPermission === "read" ? "read" : "readWrite"; await checkAuthorizationUpdated({ userId: principal.userId, diff --git a/apps/web/modules/workspaces/settings/sources/page.tsx b/apps/web/modules/workspaces/settings/sources/page.tsx deleted file mode 100644 index ccb9cb1c1e..0000000000 --- a/apps/web/modules/workspaces/settings/sources/page.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { notFound } from "next/navigation"; -import { ConnectorsSection } from "@/app/(app)/workspaces/[workspaceId]/unify/sources/components/connectors-page-client"; -import { transformToUnifySurvey } from "@/app/(app)/workspaces/[workspaceId]/unify/sources/lib"; -import { getConnectorsWithMappings } from "@/lib/connector/service"; -import { getSurveys } from "@/lib/survey/service"; -import { getTranslate } from "@/lingodotdev/server"; -import { getFeedbackRecordDirectoriesByWorkspaceId } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory"; -import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils"; - -export const WorkspaceFeedbackSourcesPage = async ( - props: Readonly<{ params: Promise<{ workspaceId: string }> }> -) => { - const t = await getTranslate(); - const params = await props.params; - - const { isOwner, isManager, hasReadAccess, hasReadWriteAccess, hasManageAccess, session } = - await getWorkspaceAuth(params.workspaceId); - - if (!session) { - throw new Error(t("common.session_not_found")); - } - - const hasAccess = isOwner || isManager || hasReadAccess || hasReadWriteAccess || hasManageAccess; - if (!hasAccess) { - return notFound(); - } - - const [connectors, surveys, directories] = await Promise.all([ - getConnectorsWithMappings(params.workspaceId), - getSurveys(params.workspaceId), - getFeedbackRecordDirectoriesByWorkspaceId(params.workspaceId), - ]); - - const unifySurveys = surveys.map(transformToUnifySurvey); - - return ( - - ); -}; diff --git a/docs/self-hosting/advanced/license.mdx b/docs/self-hosting/advanced/license.mdx index 25d5b55e76..302732ea95 100644 --- a/docs/self-hosting/advanced/license.mdx +++ b/docs/self-hosting/advanced/license.mdx @@ -87,6 +87,9 @@ The Enterprise Edition allows us to fund the development of Formbricks sustainab | Teams & access roles | ❌ | ✅ | | Contact management & segments | ❌ | ✅ | | Quota Management | ❌ | ✅ | +| Unify Feedback Inbox | ❌ | ✅ | +| Feedback Record Directories | ❌ | ✅ | +| Insights Dashboards | ❌ | ✅ | | Audit Logs | ❌ | ✅ | | OIDC SSO (AzureAD, Google, OpenID) | ❌ | ✅ | | SAML SSO | ❌ | ✅ |