moves connectors, dashboards and frd to ee

This commit is contained in:
pandeymangg
2026-05-04 12:55:34 +05:30
parent ea92ef9fce
commit 319a76a70d
66 changed files with 672 additions and 142 deletions
@@ -1 +1 @@
export { WorkspaceFeedbackSourcesPage as default } from "@/modules/workspaces/settings/sources/page";
export { WorkspaceFeedbackSourcesPage as default } from "@/modules/ee/unify-feedback/sources/page";
@@ -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 (
<FeedbackRecordsPageClient
workspaceId={params.workspaceId}
initialRecords={merged}
frdMap={frdMap}
csvSources={csvSources}
canWrite={canWrite}
/>
);
}
export { UnifyFeedbackRecordsPage as default } from "@/modules/ee/unify-feedback/page";
+9
View File
@@ -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
+10 -1
View File
@@ -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",
+10 -1
View File
@@ -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",
+10 -1
View File
@@ -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",
+10 -1
View File
@@ -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 à lespace 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",
+10 -1
View File
@@ -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ó",
+10 -1
View File
@@ -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": "ユーザー",
+10 -1
View File
@@ -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",
+10 -1
View File
@@ -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",
+10 -1
View File
@@ -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",
+10 -1
View File
@@ -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",
+10 -1
View File
@@ -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": "Пользователь",
+10 -1
View File
@@ -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",
+10 -1
View File
@@ -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ı",
+10 -1
View File
@@ -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": "用户",
+10 -1
View File
@@ -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": "使用者",
@@ -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;
@@ -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<typeof ZGetDashboardsAction>;
}) => {
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<typeof ZGetDashboardAction>;
}) => {
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,
@@ -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 (
<AnalysisPageLayout pageTitle={t("common.analysis")} workspaceId={workspaceId}>
<UpgradePrompt
title={t("workspace.dashboards.upgrade_prompt_title")}
description={t("workspace.dashboards.upgrade_prompt_description")}
feature="dashboards"
buttons={[
{
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${workspaceId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${workspaceId}/settings/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
]}
/>
</AnalysisPageLayout>
);
}
const directories = await getFeedbackRecordDirectoriesByWorkspaceId(workspaceId);
let dashboard;
@@ -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<DashboardsListPageProps>) => {
const t = await getTranslate();
const { isReadOnly } = await getWorkspaceAuth(workspaceId);
const { isReadOnly, organization } = await getWorkspaceAuth(workspaceId);
const isDashboardsAllowed = await getIsDashboardsEnabled(organization.id);
if (!isDashboardsAllowed) {
return (
<AnalysisPageLayout pageTitle={t("common.analysis")} workspaceId={workspaceId}>
<UpgradePrompt
title={t("workspace.dashboards.upgrade_prompt_title")}
description={t("workspace.dashboards.upgrade_prompt_description")}
feature="dashboards"
buttons={[
{
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${workspaceId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${workspaceId}/settings/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
]}
/>
</AnalysisPageLayout>
);
}
const [hasFeedbackRecords, connectors] = await Promise.all([
hasWorkspaceFeedbackRecords(workspaceId),
@@ -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 () => {
@@ -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,
@@ -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 (
<PageContentWrapper>
<PageHeader pageTitle={t("workspace.settings.general.organization_settings")}>
<OrganizationSettingsNavbar
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
membershipRole={currentUserMembership.role}
activeId="feedback-record-directories"
/>
</PageHeader>
<UpgradePrompt
title={t("workspace.settings.feedback_record_directories.upgrade_prompt_title")}
description={t("workspace.settings.feedback_record_directories.upgrade_prompt_description")}
feature="feedback-record-directories"
buttons={[
{
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
]}
/>
</PageContentWrapper>
);
}
if (!isOwner && !isManager) {
return (
<PageContentWrapper>
@@ -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,
},
};
@@ -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
@@ -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", () => {
+23 -1
View File
@@ -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<boolean> => {
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<boolean> => {
return getCustomPlanFeaturePermission(organizationId, "unifyFeedback");
};
export const getIsFeedbackRecordDirectoriesEnabled = async (organizationId: string): Promise<boolean> => {
return getCustomPlanFeaturePermission(organizationId, "feedbackRecordDirectories");
};
export const getIsDashboardsEnabled = async (organizationId: string): Promise<boolean> => {
return getCustomPlanFeaturePermission(organizationId, "dashboards");
};
export const getOrganizationWorkspacesLimit = async (organizationId: string): Promise<number> => {
const entitlementsContext = await getOrganizationEntitlementsContext(organizationId);
@@ -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<typeof ZEnterpriseLicenseFeatures>;
@@ -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<void> => {
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,
@@ -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;
@@ -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;
@@ -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 (
<PageContentWrapper>
<UpgradePrompt
title={t("workspace.unify.upgrade_prompt_title")}
description={t("workspace.unify.upgrade_prompt_description")}
feature="unify-feedback"
buttons={[
{
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
]}
/>
</PageContentWrapper>
);
}
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 (
<FeedbackRecordsPageClient
workspaceId={params.workspaceId}
initialRecords={merged}
frdMap={frdMap}
csvSources={csvSources}
canWrite={canWrite}
/>
);
};
@@ -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<TUnifySurvey[]> => {
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,
@@ -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 (
<PageContentWrapper>
<UpgradePrompt
title={t("workspace.unify.upgrade_prompt_title")}
description={t("workspace.unify.upgrade_prompt_description")}
feature="unify-feedback"
buttons={[
{
text: IS_FORMBRICKS_CLOUD ? t("common.upgrade_plan") : t("common.request_trial_license"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/billing`
: "https://formbricks.com/upgrade-self-hosting-license",
},
{
text: t("common.learn_more"),
href: IS_FORMBRICKS_CLOUD
? `/workspaces/${params.workspaceId}/settings/billing`
: "https://formbricks.com/learn-more-self-hosting-license",
},
]}
/>
</PageContentWrapper>
);
}
const [connectors, surveys, directories] = await Promise.all([
getConnectorsWithMappings(params.workspaceId),
getSurveys(params.workspaceId),
getFeedbackRecordDirectoriesByWorkspaceId(params.workspaceId),
]);
const unifySurveys = surveys.map(transformToUnifySurvey);
return (
<ConnectorsSection
workspaceId={params.workspaceId}
initialConnectors={connectors}
initialSurveys={unifySurveys}
directories={directories}
/>
);
};
@@ -12,6 +12,9 @@ const LICENSE_GUARDED_ENTITLEMENTS: Partial<Record<string, keyof TEnterpriseLice
contacts: "contacts",
"ai-smart-tools": "aiSmartTools",
"ai-data-analysis": "aiDataAnalysis",
"unify-feedback": "unifyFeedback",
"feedback-record-directories": "feedbackRecordDirectories",
dashboards: "dashboards",
};
const TRIAL_RESTRICTED_ENTITLEMENT_KEYS = [
@@ -33,6 +33,15 @@ const mapLicenseFeaturesToEntitlements = (
if (features.aiDataAnalysis) {
entitlementKeys.push(CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.AI_DATA_ANALYSIS);
}
if (features.unifyFeedback) {
entitlementKeys.push(CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.UNIFY_FEEDBACK);
}
if (features.feedbackRecordDirectories) {
entitlementKeys.push(CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.FEEDBACK_RECORD_DIRECTORIES);
}
if (features.dashboards) {
entitlementKeys.push(CLOUD_STRIPE_FEATURE_LOOKUP_KEYS.DASHBOARDS);
}
return entitlementKeys;
};
@@ -8,16 +8,15 @@ import { ZId } from "@formbricks/types/common";
import { AuthorizationError } from "@formbricks/types/errors";
import { verifyFeedbackRecordsGatewayToken } from "@/lib/jwt";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
import { getBearerTokenFromHeaders } from "@/modules/api/lib/api-key-auth";
import { getFeedbackRecordDirectoryAuthContext } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory";
import { getIsUnifyFeedbackEnabled } from "@/modules/ee/license-check/lib/utils";
import {
getBearerTokenFromHeaders,
} from "@/modules/api/lib/api-key-auth";
import {
buildAllowResponse,
buildStatusResponse,
TEnvoyAuthenticatedPrincipal,
TEnvoyRequestAuthorizer,
buildAllowResponse,
buildStatusResponse,
} from "@/modules/envoy-auth/shared";
import { getFeedbackRecordDirectoryAuthContext } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory";
import { getFeedbackRecordTenant } from "@/modules/hub/service";
const FEEDBACK_RECORDS_V3_PREFIX = "/api/v3/feedbackRecords";
@@ -79,10 +78,7 @@ const normalizeFeedbackRecordsPath = (pathname: string): string | null => {
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,
@@ -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 (
<ConnectorsSection
workspaceId={params.workspaceId}
initialConnectors={connectors}
initialSurveys={unifySurveys}
directories={directories}
/>
);
};
+3
View File
@@ -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 | ❌ | ✅ |