feat: feedback record directories

This commit is contained in:
pandeymangg
2026-03-25 18:38:38 +05:30
parent 81272b96e1
commit af92621a93
34 changed files with 1805 additions and 36 deletions
@@ -138,6 +138,12 @@ export const OrganizationBreadcrumb = ({
label: t("common.members_and_teams"),
href: `/environments/${currentEnvironmentId}/settings/teams`,
},
{
id: "feedback-record-directories",
label: t("environments.settings.feedback_record_directories.nav_label"),
href: `/environments/${currentEnvironmentId}/settings/feedback-record-directories`,
hidden: isMember,
},
{
id: "api-keys",
label: t("common.api_keys"),
@@ -40,6 +40,13 @@ export const OrganizationSettingsNavbar = ({
href: `/environments/${environmentId}/settings/teams`,
current: pathname?.includes("/teams"),
},
{
id: "feedback-record-directories",
label: t("environments.settings.feedback_record_directories.nav_label"),
href: `/environments/${environmentId}/settings/feedback-record-directories`,
current: pathname?.includes("/feedback-record-directories"),
hidden: isMember,
},
{
id: "api-keys",
label: t("common.api_keys"),
@@ -0,0 +1,3 @@
import { FeedbackRecordDirectoriesPage } from "@/modules/ee/feedback-record-directory/page";
export default FeedbackRecordDirectoriesPage;
+37
View File
@@ -91,6 +91,7 @@ checksums:
common/action: c92af0bdf1698b0d10cf5b28d2ad4945
common/actions: c46571856723b03262fd33f511116298
common/actions_description: 8e35b1538d1006fa8470183310ad21ef
common/active: 3e1ec025c4a50830bbb9ad57a176630a
common/active_surveys: 95bf0fd5d2bf62cdd010b7cf66795ed7
common/activity: 1948763de8e531483a798b68195e297e
common/add: 87c4a663507f2bcbbf79934af8164e13
@@ -112,6 +113,7 @@ checksums:
common/app: 77e32ac4e5a1e01bc9a6a15fdfef9bf8
common/app_survey: f076d131d20bfdadb35fba29c8275232
common/apply_filters: 6543c1e80038b3da0f4a42848d08d4d1
common/archived: cf5127ecfd7e43a35466a1ba5fe16450
common/are_you_sure: 6d5cd13628a7887711fd0c29f1123652
common/attributes: 86d0ae6fea0fbb119722ed3841f8385a
common/back: f541015a827e37cb3b1234e56bc2aa3c
@@ -154,6 +156,7 @@ checksums:
common/count_questions: a7a34376a01eda781381fe7544541293
common/count_responses: 437e022825c7a08481d8f7e56926742d
common/count_selections: a1ec41682b9a7d8601c3905dfba34e16
common/create: 757ccd28dd533ff3a933355273c1e32a
common/create_new_organization: 51dae7b33143686ee218abf5bea764a5
common/create_segment: 9d8291cd4d778b53b73bbc84fd91c181
common/create_survey: 1cfbba08d34876566d84b2960054a987
@@ -809,8 +812,14 @@ checksums:
environments/integrations/webhooks/created_by_third_party: b40197eabbbce500b80b44268b8b1ee9
environments/integrations/webhooks/discord_webhook_not_supported: 23432534f908b2ba63a517fb1f9bbe0e
environments/integrations/webhooks/empty_webhook_message: 4c4d8709576a38cb8eb59866331d2405
environments/integrations/webhooks/endpoint_bad_gateway_error: 48ab17e9a77030b289ec22f497f50b63
environments/integrations/webhooks/endpoint_gateway_timeout_error: 5da45e2f6933927d1f8b0aaa9566e6a6
environments/integrations/webhooks/endpoint_internal_server_error: 6773fc34349febf95475cde88d8ee072
environments/integrations/webhooks/endpoint_method_not_allowed_error: 9963b503311393f4d7bffae9df46d422
environments/integrations/webhooks/endpoint_not_found_error: 607b75b7b7aa92ca81fe44e466f7c318
environments/integrations/webhooks/endpoint_pinged: 3b1fce00e61d4b9d2bdca390649c58b6
environments/integrations/webhooks/endpoint_pinged_error: 96c312fe8214757c4a934cdfbe177027
environments/integrations/webhooks/endpoint_service_unavailable_error: f9d4874c322f2963f5afaede354c9416
environments/integrations/webhooks/learn_to_verify: 25b2a035e2109170b28f4e16db76ad39
environments/integrations/webhooks/no_triggers: 6b68cddfc45b3f7e20644a24a1bbea69
environments/integrations/webhooks/please_check_console: 7b1787e82a0d762df02c011ebb1650ea
@@ -1067,6 +1076,32 @@ checksums:
environments/settings/enterprise/sso: 95e98e279bb89233d63549b202bd9112
environments/settings/enterprise/teams: 21ab78abcba0f16c3029741563f789ea
environments/settings/enterprise/unlock_the_full_power_of_formbricks_free_for_30_days: 104d07b63a42911c9673ceb08a4dbd43
environments/settings/feedback_record_directories/all_workspaces_added: 0e726cacab775953965c3e39235d0cad
environments/settings/feedback_record_directories/archive: fa813ab3074103e5daad07462af25789
environments/settings/feedback_record_directories/archive_directory: 1114a8e1c5c5af3d30a174b28582f424
environments/settings/feedback_record_directories/archive_not_allowed: 3ffe3336572a633406858887de60a470
environments/settings/feedback_record_directories/are_you_sure_you_want_to_archive: d249e6e8bc0345835a13f70856eb1c30
environments/settings/feedback_record_directories/assign_workspaces_description: 6c3f0bbf3bd7744bb313f4cd7886e184
environments/settings/feedback_record_directories/create_directory: 9744e2395a5350bd555a7b3b5e1b763a
environments/settings/feedback_record_directories/create_new_directory: 912bd70bb19e90512c9904bd6ff15932
environments/settings/feedback_record_directories/description: 8f56b169cb38d8c7b2697bf3a3ed7a61
environments/settings/feedback_record_directories/directory: 1568e9b2f954aae4edb9e3a2503943af
environments/settings/feedback_record_directories/directory_archived_successfully: fba5b99ced59d0546c8f2241c092a5dd
environments/settings/feedback_record_directories/directory_created_successfully: 5db20153b840d91842543f71cdd91043
environments/settings/feedback_record_directories/directory_id: 933a1376d7d8a8dc41ded90ef1c0f619
environments/settings/feedback_record_directories/directory_name: 353de006e16451bf64da469a81fbe451
environments/settings/feedback_record_directories/directory_settings_description: 895d890b4292effa5ade45fe7164990f
environments/settings/feedback_record_directories/directory_settings_title: c32af860e3254dea9dfaeefc7cd92d49
environments/settings/feedback_record_directories/directory_unarchived_successfully: 08d56e260decc62fe664b50ab774b728
environments/settings/feedback_record_directories/directory_updated_successfully: 638cb6c92f535328d809274cf2be4d7d
environments/settings/feedback_record_directories/empty_state: 665593dcb7cfa081a3e719677d0f6b0d
environments/settings/feedback_record_directories/enter_directory_name: a1c950988199bb4c4e014dcf430cce41
environments/settings/feedback_record_directories/nav_label: cf9a57b3cbac0f04b98e06fb693e986e
environments/settings/feedback_record_directories/no_access: cc3385cd01a11e3949003a2cc6fb5b31
environments/settings/feedback_record_directories/please_fill_all_workspace_fields: f5a9f4e4b011c29f96eb10230165a64b
environments/settings/feedback_record_directories/show_archived: c4c1c3bbddc1bb1540c079b589a2d3de
environments/settings/feedback_record_directories/title: e3d425c27f80162f29ce094e31a3fd8f
environments/settings/feedback_record_directories/unarchive: 671fc7e9d7c8cb4d182a25a46551c168
environments/settings/general/bulk_invite_warning_description: e8737a2fbd5ff353db5580d17b4b5a37
environments/settings/general/cannot_delete_only_organization: 833cc6848b28f2694a4552b4de91a6ba
environments/settings/general/cannot_leave_only_organization: dd8463262e4299fef7ad73512225c55b
@@ -1608,6 +1643,8 @@ checksums:
environments/surveys/edit/response_limit_needs_to_exceed_number_of_received_responses: 9a9c223c0918ded716ddfaa84fbaa8d9
environments/surveys/edit/response_limits_redirections_and_more: e4f1cf94e56ad0e1b08701158d688802
environments/surveys/edit/response_options: 2988136d5248d7726583108992dcbaee
environments/surveys/edit/reverse_order_occasionally: 170fd50de940f382fa2e605228e4e088
environments/surveys/edit/reverse_order_occasionally_except_last: 1c833001b940f1419dd7534b199a0b4a
environments/surveys/edit/roundness: 5a161c8f5f258defb57ed1d551737cc4
environments/surveys/edit/roundness_description: 03940a6871ae43efa4810cba7cadb74b
environments/surveys/edit/row_used_in_logic_error: f89453ff1b6db77ad84af840fedd9813
@@ -22,6 +22,7 @@ export type AuditLoggingCtx = {
quotaId?: string;
teamId?: string;
integrationId?: string;
feedbackRecordDirectoryId?: string;
};
export type ActionClientCtx = {
+31
View File
@@ -118,6 +118,7 @@
"action": "Aktion",
"actions": "Aktionen",
"actions_description": "Code- und No-Code-Aktionen werden verwendet, um Abfangumfragen innerhalb von Apps und auf Websites auszulösen.",
"active": "Aktiv",
"active_surveys": "Aktive Umfragen",
"activity": "Aktivität",
"add": "Hinzufügen",
@@ -139,6 +140,7 @@
"app": "App",
"app_survey": "App-Umfrage",
"apply_filters": "Filter anwenden",
"archived": "Archiviert",
"are_you_sure": "Bist Du sicher?",
"attributes": "Attribute",
"back": "Zurück",
@@ -181,6 +183,7 @@
"count_questions": "{count, plural, one {{count} Frage} other {{count} Fragen}}",
"count_responses": "{count, plural, one {{count} Antwort} other {{count} Antworten}}",
"count_selections": "{count, plural, one {{count} Auswahl} other {{count} Auswahlen}}",
"create": "Erstellen",
"create_new_organization": "Neue Organisation erstellen",
"create_segment": "Segment erstellen",
"create_survey": "Umfrage erstellen",
@@ -1133,6 +1136,34 @@
"teams": "Teams & Zugriffskontrolle (Lesen, Lesen & Schreiben, Verwalten)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Schalte die volle Power von Formbricks frei. 30 Tage kostenlos."
},
"feedback_record_directories": {
"all_workspaces_added": "Alle Workspaces wurden zu diesem Verzeichnis hinzugefügt.",
"archive": "Archivieren",
"archive_directory": "Verzeichnis archivieren",
"archive_not_allowed": "Du darfst dieses Verzeichnis nicht archivieren.",
"are_you_sure_you_want_to_archive": "Möchtest du dieses Verzeichnis wirklich archivieren? Workspaces haben dann keinen Zugriff mehr darauf.",
"assign_workspaces_description": "Lege fest, welche Workspaces auf dieses Feedback-Verzeichnis zugreifen können.",
"create_directory": "Verzeichnis erstellen",
"create_new_directory": "Neues Feedback-Verzeichnis erstellen",
"description": "Verwalte Feedback-Verzeichnisse und ihre Workspace-Zuordnungen.",
"directory": "Verzeichnis",
"directory_archived_successfully": "Verzeichnis erfolgreich archiviert",
"directory_created_successfully": "Verzeichnis erfolgreich erstellt",
"directory_id": "Verzeichnis-ID",
"directory_name": "Verzeichnisname",
"directory_settings_description": "Verwalte Verzeichnisnamen, Workspace-Zuordnungen und mehr.",
"directory_settings_title": "Einstellungen für {directoryName}",
"directory_unarchived_successfully": "Archivierung des Verzeichnisses erfolgreich aufgehoben",
"directory_updated_successfully": "Verzeichnis erfolgreich aktualisiert",
"empty_state": "Keine Feedback-Verzeichnisse gefunden. Erstelle eins, um loszulegen.",
"enter_directory_name": "Verzeichnisnamen eingeben",
"nav_label": "Feedback-Verzeichnisse",
"no_access": "Du hast keine Berechtigung, Feedback-Verzeichnisse zu verwalten.",
"please_fill_all_workspace_fields": "Bitte fülle zuerst alle Workspace-Felder aus.",
"show_archived": "Archivierte anzeigen",
"title": "Feedback-Aufzeichnungsverzeichnisse",
"unarchive": "Aus Archiv wiederherstellen"
},
"general": {
"bulk_invite_warning_description": "Bitte beachte, dass im Free-Plan alle Organisationsmitglieder automatisch die Rolle \"Owner\" zugewiesen bekommen, unabhängig von der im CSV-File angegebenen Rolle.",
"cannot_delete_only_organization": "Das ist deine einzige Organisation, sie kann nicht gelöscht werden. Erstelle zuerst eine neue Organisation.",
+34 -3
View File
@@ -118,6 +118,7 @@
"action": "Action",
"actions": "Actions",
"actions_description": "Code and No-Code Actions are used to trigger intercept surveys within apps & on websites.",
"active": "Active",
"active_surveys": "Active surveys",
"activity": "Activity",
"add": "Add",
@@ -139,6 +140,7 @@
"app": "App",
"app_survey": "App Survey",
"apply_filters": "Apply filters",
"archived": "Archived",
"are_you_sure": "Are you sure?",
"attributes": "Attributes",
"back": "Back",
@@ -181,6 +183,7 @@
"count_questions": "{count, plural, one {{count} question} other {{count} questions}}",
"count_responses": "{count, plural, one {{count} response} other {{count} responses}}",
"count_selections": "{count, plural, one {{count} selection} other {{count} selections}}",
"create": "Create",
"create_new_organization": "Create new organization",
"create_segment": "Create segment",
"create_survey": "Create survey",
@@ -261,11 +264,11 @@
"invalid_file_type": "Invalid file type",
"invite": "Invite",
"invite_them": "Invite them",
"javascript_required": "JavaScript Required",
"javascript_required_description": "Formbricks requires JavaScript to function properly. Please enable JavaScript in your browser settings to continue.",
"key": "Key",
"label": "Label",
"language": "Language",
"javascript_required": "JavaScript Required",
"javascript_required_description": "Formbricks requires JavaScript to function properly. Please enable JavaScript in your browser settings to continue.",
"last_name": "Last Name",
"learn_more": "Learn more",
"license_expired": "License Expired",
@@ -1133,6 +1136,34 @@
"teams": "Teams & Access Roles (Read, Read & Write, Manage)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Unlock the full power of Formbricks. Free for 30 days."
},
"feedback_record_directories": {
"all_workspaces_added": "All workspaces added to this directory.",
"archive": "Archive",
"archive_directory": "Archive Directory",
"archive_not_allowed": "You are not allowed to archive this directory.",
"are_you_sure_you_want_to_archive": "Are you sure you want to archive this directory? Workspaces will no longer have access to it.",
"assign_workspaces_description": "Control which workspaces can access this feedback record directory.",
"create_directory": "Create Directory",
"create_new_directory": "Create new feedback record directory",
"description": "Manage feedback record directories and their workspace assignments.",
"directory": "directory",
"directory_archived_successfully": "Directory archived successfully",
"directory_created_successfully": "Directory created successfully",
"directory_id": "Directory ID",
"directory_name": "Directory Name",
"directory_settings_description": "Manage directory name, workspace assignments, and more.",
"directory_settings_title": "{directoryName} Settings",
"directory_unarchived_successfully": "Directory unarchived successfully",
"directory_updated_successfully": "Directory updated successfully",
"empty_state": "No feedback record directories found. Create one to get started.",
"enter_directory_name": "Enter directory name",
"nav_label": "Feedback Directories",
"no_access": "You do not have permission to manage feedback record directories.",
"please_fill_all_workspace_fields": "Please fill all workspace fields first.",
"show_archived": "Show archived",
"title": "Feedback Record Directories",
"unarchive": "Unarchive"
},
"general": {
"bulk_invite_warning_description": "On the free plan, all organization members are always assigned the “Owner” role.",
"cannot_delete_only_organization": "This is your only organization, it cannot be deleted. Create a new organization first.",
@@ -3354,4 +3385,4 @@
"thank_you_description": "Your input helps us build the Workflows feature you actually need. We will keep you posted on our progress.",
"thank_you_title": "Thank you for your feedback!"
}
}
}
+31
View File
@@ -118,6 +118,7 @@
"action": "Acción",
"actions": "Acciones",
"actions_description": "Las acciones de código y sin código se utilizan para activar encuestas de intercepción en aplicaciones y sitios web.",
"active": "Activo",
"active_surveys": "Encuestas activas",
"activity": "Actividad",
"add": "Añadir",
@@ -139,6 +140,7 @@
"app": "Aplicación",
"app_survey": "Encuesta de aplicación",
"apply_filters": "Aplicar filtros",
"archived": "Archivado",
"are_you_sure": "¿Estás seguro?",
"attributes": "Atributos",
"back": "Atrás",
@@ -181,6 +183,7 @@
"count_questions": "{count, plural, one {{count} pregunta} other {{count} preguntas}}",
"count_responses": "{count, plural, one {{count} respuesta} other {{count} respuestas}}",
"count_selections": "{count, plural, one {{count} selección} other {{count} selecciones}}",
"create": "Crear",
"create_new_organization": "Crear organización nueva",
"create_segment": "Crear segmento",
"create_survey": "Crear encuesta",
@@ -1133,6 +1136,34 @@
"teams": "Equipos y roles de acceso (lectura, lectura y escritura, gestión)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Desbloquea todo el potencial de Formbricks. Gratis durante 30 días."
},
"feedback_record_directories": {
"all_workspaces_added": "Todos los espacios de trabajo añadidos a este directorio.",
"archive": "Archivar",
"archive_directory": "Archivar Directorio",
"archive_not_allowed": "No tienes permiso para archivar este directorio.",
"are_you_sure_you_want_to_archive": "¿Estás seguro de que quieres archivar este directorio? Los espacios de trabajo ya no tendrán acceso a él.",
"assign_workspaces_description": "Controla qué espacios de trabajo pueden acceder a este directorio de registros de feedback.",
"create_directory": "Crear Directorio",
"create_new_directory": "Crear nuevo directorio de registros de feedback",
"description": "Gestiona los directorios de registros de feedback y sus asignaciones de espacios de trabajo.",
"directory": "directorio",
"directory_archived_successfully": "Directorio archivado correctamente",
"directory_created_successfully": "Directorio creado correctamente",
"directory_id": "ID del Directorio",
"directory_name": "Nombre del Directorio",
"directory_settings_description": "Gestiona el nombre del directorio, las asignaciones de espacios de trabajo y más.",
"directory_settings_title": "Configuración de {directoryName}",
"directory_unarchived_successfully": "Directorio desarchivado correctamente",
"directory_updated_successfully": "Directorio actualizado correctamente",
"empty_state": "No se encontraron directorios de registros de feedback. Crea uno para empezar.",
"enter_directory_name": "Introduce el nombre del directorio",
"nav_label": "Directorios de Feedback",
"no_access": "No tienes permiso para gestionar los directorios de registros de feedback.",
"please_fill_all_workspace_fields": "Por favor, completa primero todos los campos del espacio de trabajo.",
"show_archived": "Mostrar archivados",
"title": "Directorios de Registros de Feedback",
"unarchive": "Desarchivar"
},
"general": {
"bulk_invite_warning_description": "En el plan gratuito, a todos los miembros de la organización se les asigna siempre el rol de \"Propietario\".",
"cannot_delete_only_organization": "Esta es tu única organización, no se puede eliminar. Crea una nueva organización primero.",
+31
View File
@@ -118,6 +118,7 @@
"action": "Action",
"actions": "Actions",
"actions_description": "Les actions avec et sans code permettent de déclencher des enquêtes dans des applications et sur des sites Web.",
"active": "Actif",
"active_surveys": "Sondages actifs",
"activity": "Activité",
"add": "Ajouter",
@@ -139,6 +140,7 @@
"app": "Application",
"app_survey": "Sondage d'application",
"apply_filters": "Appliquer des filtres",
"archived": "Archivé",
"are_you_sure": "Es-tu sûr ?",
"attributes": "Attributs",
"back": "Retour",
@@ -181,6 +183,7 @@
"count_questions": "{count, plural, one {{count} question} other {{count} questions}}",
"count_responses": "{count, plural, one {{count} réponse} other {{count} réponses}}",
"count_selections": "{count, plural, one {{count} sélection} other {{count} sélections}}",
"create": "Créer",
"create_new_organization": "Créer une nouvelle organisation",
"create_segment": "Créer un segment",
"create_survey": "Créer un sondage",
@@ -1133,6 +1136,34 @@
"teams": "Équipes et Rôles d'Accès (Lire, Lire et Écrire, Gérer)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Débloquez tout le potentiel de Formbricks. Gratuit pendant 30 jours."
},
"feedback_record_directories": {
"all_workspaces_added": "Tous les espaces de travail ont été ajoutés à ce répertoire.",
"archive": "Archiver",
"archive_directory": "Archiver le répertoire",
"archive_not_allowed": "Vous n'êtes pas autorisé à archiver ce répertoire.",
"are_you_sure_you_want_to_archive": "Es-tu sûr de vouloir archiver ce répertoire ? Les espaces de travail n'y auront plus accès.",
"assign_workspaces_description": "Contrôle quels espaces de travail peuvent accéder à ce répertoire de feedback.",
"create_directory": "Créer un répertoire",
"create_new_directory": "Créer un nouveau répertoire de feedback",
"description": "Gère les répertoires de feedback et leurs affectations aux espaces de travail.",
"directory": "répertoire",
"directory_archived_successfully": "Répertoire archivé avec succès",
"directory_created_successfully": "Répertoire créé avec succès",
"directory_id": "ID du répertoire",
"directory_name": "Nom du répertoire",
"directory_settings_description": "Gère le nom du répertoire, les affectations aux espaces de travail et plus encore.",
"directory_settings_title": "Paramètres de {directoryName}",
"directory_unarchived_successfully": "Répertoire désarchivé avec succès",
"directory_updated_successfully": "Répertoire mis à jour avec succès",
"empty_state": "Aucun répertoire de feedback trouvé. Crée-en un pour commencer.",
"enter_directory_name": "Saisir le nom du répertoire",
"nav_label": "Répertoires de feedback",
"no_access": "Tu n'as pas la permission de gérer les répertoires de feedback.",
"please_fill_all_workspace_fields": "Veuillez d'abord remplir tous les champs de l'espace de travail.",
"show_archived": "Afficher les éléments archivés",
"title": "Répertoires d'enregistrement des retours",
"unarchive": "Désarchiver"
},
"general": {
"bulk_invite_warning_description": "Dans le plan gratuit, tous les membres de l'organisation se voient toujours attribuer le rôle \"Owner\".",
"cannot_delete_only_organization": "C'est votre seule organisation, elle ne peut pas être supprimée. Créez d'abord une nouvelle organisation.",
+31
View File
@@ -118,6 +118,7 @@
"action": "Művelet",
"actions": "Műveletek",
"actions_description": "A kód vagy kód nélküli műveleteket arra használják, hogy aktiválják a kérdőívek alkalmazásokon és webhelyeken belüli elfogását.",
"active": "Aktív",
"active_surveys": "Aktív kérdőívek",
"activity": "Tevékenység",
"add": "Hozzáadás",
@@ -139,6 +140,7 @@
"app": "Alkalmazás",
"app_survey": "Alkalmazás-kérdőív",
"apply_filters": "Szűrők alkalmazása",
"archived": "Archivált",
"are_you_sure": "Biztos benne?",
"attributes": "Attribútumok",
"back": "Vissza",
@@ -181,6 +183,7 @@
"count_questions": "{count, plural, one {{count} kérdés} other {{count} kérdés}}",
"count_responses": "{count, plural, one {{count} válasz} other {{count} válasz}}",
"count_selections": "{count, plural, one {{count} kiválasztás} other {{count} kiválasztás}}",
"create": "Létrehozás",
"create_new_organization": "Új szervezet létrehozása",
"create_segment": "Szakasz létrehozása",
"create_survey": "Kérdőív létrehozása",
@@ -1133,6 +1136,34 @@
"teams": "Csapatok és hozzáférési szerepek (olvasás, olvasás és írás, kezelés)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "A Formbricks teljes erejének feloldása. 30 napig ingyen."
},
"feedback_record_directories": {
"all_workspaces_added": "Minden munkaterület hozzáadva ehhez a könyvtárhoz.",
"archive": "Archiválás",
"archive_directory": "Könyvtár archiválása",
"archive_not_allowed": "Nem rendelkezik jogosultsággal ezen könyvtár archiválásához.",
"are_you_sure_you_want_to_archive": "Biztosan archiválni kívánja ezt a könyvtárat? A munkaterületek többé nem férhetnek hozzá.",
"assign_workspaces_description": "Szabályozza, mely munkaterületek férhetnek hozzá ehhez a visszajelzési nyilvántartási könyvtárhoz.",
"create_directory": "Könyvtár létrehozása",
"create_new_directory": "Új visszajelzési nyilvántartási könyvtár létrehozása",
"description": "Visszajelzési nyilvántartási könyvtárak és munkaterület-hozzárendeléseik kezelése.",
"directory": "könyvtár",
"directory_archived_successfully": "A könyvtár sikeresen archiválva",
"directory_created_successfully": "A könyvtár sikeresen létrehozva",
"directory_id": "Könyvtár azonosító",
"directory_name": "Könyvtár neve",
"directory_settings_description": "Könyvtár nevének, munkaterület-hozzárendeléseinek és egyéb beállítások kezelése.",
"directory_settings_title": "{directoryName} beállításai",
"directory_unarchived_successfully": "A könyvtár archiválása sikeresen visszavonva",
"directory_updated_successfully": "A könyvtár sikeresen frissítve",
"empty_state": "Nem található visszajelzési nyilvántartási könyvtár. Hozzon létre egyet a kezdéshez.",
"enter_directory_name": "Adja meg a könyvtár nevét",
"nav_label": "Visszajelzési könyvtárak",
"no_access": "Nem rendelkezik jogosultsággal a visszajelzési nyilvántartási könyvtárak kezeléséhez.",
"please_fill_all_workspace_fields": "Kérem, először töltse ki az összes munkaterület mezőt.",
"show_archived": "Archivált elemek megjelenítése",
"title": "Visszajelzési Nyilvántartási Könyvtárak",
"unarchive": "Archiválás visszavonása"
},
"general": {
"bulk_invite_warning_description": "Az ingyenes csomagban az összes szervezeti tag mindig a „Tulajdonos” szerephez van hozzárendelve.",
"cannot_delete_only_organization": "Ez az egyetlen szervezete, nem lehet törölni. Először hozzon létre egy új szervezetet.",
+31
View File
@@ -118,6 +118,7 @@
"action": "アクション",
"actions": "アクション",
"actions_description": "コードとノーコードアクションは、アプリ内やウェブサイト上で調査を発動するために使用されます。",
"active": "アクティブ",
"active_surveys": "アクティブなフォーム",
"activity": "アクティビティ",
"add": "追加",
@@ -139,6 +140,7 @@
"app": "アプリ",
"app_survey": "アプリ内フォーム",
"apply_filters": "フィルターを適用",
"archived": "アーカイブ済み",
"are_you_sure": "よろしいですか?",
"attributes": "属性",
"back": "戻る",
@@ -181,6 +183,7 @@
"count_questions": "{count, plural, other {# 件の質問}}",
"count_responses": "{count, plural, other {{count} 件の回答}}",
"count_selections": "{count, plural, other {{count} 件の選択}}",
"create": "作成",
"create_new_organization": "新しい組織を作成",
"create_segment": "セグメントを作成",
"create_survey": "フォームを作成",
@@ -1133,6 +1136,34 @@
"teams": "チーム&アクセスロール(読み取り、読み書き、管理)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Formbricksの全機能をアンロック。30日間無料。"
},
"feedback_record_directories": {
"all_workspaces_added": "すべてのワークスペースがこのディレクトリに追加されました。",
"archive": "アーカイブ",
"archive_directory": "ディレクトリをアーカイブ",
"archive_not_allowed": "このディレクトリをアーカイブする権限がありません。",
"are_you_sure_you_want_to_archive": "このディレクトリをアーカイブしてもよろしいですか?ワークスペースはアクセスできなくなります。",
"assign_workspaces_description": "このフィードバック記録ディレクトリにアクセスできるワークスペースを管理します。",
"create_directory": "ディレクトリを作成",
"create_new_directory": "新しいフィードバック記録ディレクトリを作成",
"description": "フィードバック記録ディレクトリとワークスペースの割り当てを管理します。",
"directory": "ディレクトリ",
"directory_archived_successfully": "ディレクトリをアーカイブしました",
"directory_created_successfully": "ディレクトリを作成しました",
"directory_id": "ディレクトリID",
"directory_name": "ディレクトリ名",
"directory_settings_description": "ディレクトリ名、ワークスペースの割り当てなどを管理します。",
"directory_settings_title": "{directoryName}の設定",
"directory_unarchived_successfully": "ディレクトリのアーカイブを解除しました",
"directory_updated_successfully": "ディレクトリを更新しました",
"empty_state": "フィードバック記録ディレクトリが見つかりません。最初のディレクトリを作成してください。",
"enter_directory_name": "ディレクトリ名を入力してください",
"nav_label": "フィードバックディレクトリ",
"no_access": "フィードバック記録ディレクトリを管理する権限がありません。",
"please_fill_all_workspace_fields": "まず、すべてのワークスペースフィールドを入力してください。",
"show_archived": "アーカイブ済みを表示",
"title": "フィードバック記録ディレクトリ",
"unarchive": "アーカイブ解除"
},
"general": {
"bulk_invite_warning_description": "無料プランでは、すべての組織メンバーに常に「オーナー」ロールが割り当てられます。",
"cannot_delete_only_organization": "これはあなたの唯一の組織です。削除できません。まず新しい組織を作成してください。",
+31
View File
@@ -118,6 +118,7 @@
"action": "Actie",
"actions": "Acties",
"actions_description": "Code- en no-code-acties worden gebruikt om onderscheppingsenquêtes in apps en op websites te activeren.",
"active": "Actief",
"active_surveys": "Actieve enquêtes",
"activity": "Activiteit",
"add": "Toevoegen",
@@ -139,6 +140,7 @@
"app": "App",
"app_survey": "App-enquête",
"apply_filters": "Pas filters toe",
"archived": "Gearchiveerd",
"are_you_sure": "Weet je het zeker?",
"attributes": "Kenmerken",
"back": "Rug",
@@ -181,6 +183,7 @@
"count_questions": "{count, plural, one {{count} vraag} other {{count} vragen}}",
"count_responses": "{count, plural, one {{count} reactie} other {{count} reacties}}",
"count_selections": "{count, plural, one {{count} selectie} other {{count} selecties}}",
"create": "Aanmaken",
"create_new_organization": "Creëer een nieuwe organisatie",
"create_segment": "Segment maken",
"create_survey": "Enquête maken",
@@ -1133,6 +1136,34 @@
"teams": "Teams en toegangsrollen (lezen, lezen en schrijven, beheren)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Ontgrendel de volledige kracht van Formbricks. 30 dagen gratis."
},
"feedback_record_directories": {
"all_workspaces_added": "Alle workspaces zijn toegevoegd aan deze map.",
"archive": "Archiveren",
"archive_directory": "Map archiveren",
"archive_not_allowed": "Je hebt geen toestemming om deze map te archiveren.",
"are_you_sure_you_want_to_archive": "Weet je zeker dat je deze map wilt archiveren? Workspaces hebben er dan geen toegang meer toe.",
"assign_workspaces_description": "Bepaal welke workspaces toegang hebben tot deze feedbackregistratiemap.",
"create_directory": "Map aanmaken",
"create_new_directory": "Nieuwe feedbackregistratiemap aanmaken",
"description": "Beheer feedbackregistratiemappen en hun workspace-toewijzingen.",
"directory": "map",
"directory_archived_successfully": "Map succesvol gearchiveerd",
"directory_created_successfully": "Map succesvol aangemaakt",
"directory_id": "Map-ID",
"directory_name": "Mapnaam",
"directory_settings_description": "Beheer mapnaam, workspace-toewijzingen en meer.",
"directory_settings_title": "Instellingen voor {directoryName}",
"directory_unarchived_successfully": "Map succesvol gedearchiveerd",
"directory_updated_successfully": "Map succesvol bijgewerkt",
"empty_state": "Geen feedbackregistratiemappen gevonden. Maak er een aan om te beginnen.",
"enter_directory_name": "Voer mapnaam in",
"nav_label": "Feedbackmappen",
"no_access": "Je hebt geen toestemming om feedbackregistratiemappen te beheren.",
"please_fill_all_workspace_fields": "Vul eerst alle werkruimte-velden in.",
"show_archived": "Gearchiveerde weergeven",
"title": "Feedbackregistratiemappen",
"unarchive": "Dearchiveren"
},
"general": {
"bulk_invite_warning_description": "Bij het gratis abonnement krijgen alle organisatieleden altijd de rol 'Eigenaar' toegewezen.",
"cannot_delete_only_organization": "Dit is uw enige organisatie. Deze kan niet worden verwijderd. Maak eerst een nieuwe organisatie aan.",
+31
View File
@@ -118,6 +118,7 @@
"action": "Ação",
"actions": "Ações",
"actions_description": "Ações de Código e Sem Código são usadas para acionar interceptar pesquisas dentro de apps & em sites.",
"active": "Ativo",
"active_surveys": "Pesquisas ativas",
"activity": "Atividade",
"add": "Adicionar",
@@ -139,6 +140,7 @@
"app": "app",
"app_survey": "Pesquisa de App",
"apply_filters": "Aplicar filtros",
"archived": "Arquivado",
"are_you_sure": "Certeza?",
"attributes": "atributos",
"back": "Voltar",
@@ -181,6 +183,7 @@
"count_questions": "{count, plural, one {{count} pergunta} other {{count} perguntas}}",
"count_responses": "{count, plural, one {{count} resposta} other {{count} respostas}}",
"count_selections": "{count, plural, one {{count} seleção} other {{count} seleções}}",
"create": "Criar",
"create_new_organization": "Criar nova organização",
"create_segment": "Criar segmento",
"create_survey": "Criar pesquisa",
@@ -1133,6 +1136,34 @@
"teams": "Equipes e Funções de Acesso (Ler, Ler e Escrever, Gerenciar)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Desbloqueie todo o poder do Formbricks. Grátis por 30 dias."
},
"feedback_record_directories": {
"all_workspaces_added": "Todos os espaços de trabalho adicionados a este diretório.",
"archive": "Arquivar",
"archive_directory": "Arquivar Diretório",
"archive_not_allowed": "Você não tem permissão para arquivar este diretório.",
"are_you_sure_you_want_to_archive": "Tem certeza de que deseja arquivar este diretório? Os espaços de trabalho não terão mais acesso a ele.",
"assign_workspaces_description": "Controle quais espaços de trabalho podem acessar este diretório de registros de feedback.",
"create_directory": "Criar Diretório",
"create_new_directory": "Criar novo diretório de registros de feedback",
"description": "Gerencie diretórios de registros de feedback e suas atribuições de espaços de trabalho.",
"directory": "diretório",
"directory_archived_successfully": "Diretório arquivado com sucesso",
"directory_created_successfully": "Diretório criado com sucesso",
"directory_id": "ID do Diretório",
"directory_name": "Nome do Diretório",
"directory_settings_description": "Gerencie o nome do diretório, atribuições de espaços de trabalho e muito mais.",
"directory_settings_title": "Configurações de {directoryName}",
"directory_unarchived_successfully": "Diretório desarquivado com sucesso",
"directory_updated_successfully": "Diretório atualizado com sucesso",
"empty_state": "Nenhum diretório de registros de feedback encontrado. Crie um para começar.",
"enter_directory_name": "Digite o nome do diretório",
"nav_label": "Diretórios de Feedback",
"no_access": "Você não tem permissão para gerenciar diretórios de registros de feedback.",
"please_fill_all_workspace_fields": "Por favor, preencha todos os campos do workspace primeiro.",
"show_archived": "Mostrar arquivados",
"title": "Diretórios de Registros de Feedback",
"unarchive": "Desarquivar"
},
"general": {
"bulk_invite_warning_description": "Por favor, note que no Plano Gratuito, todos os membros da organização são automaticamente atribuídos ao papel de 'Owner', independentemente do papel especificado no arquivo CSV.",
"cannot_delete_only_organization": "Essa é sua única organização, não pode ser deletada. Crie uma nova organização primeiro.",
+31
View File
@@ -118,6 +118,7 @@
"action": "Ação",
"actions": "Ações",
"actions_description": "As ações com código e sem código são usadas para acionar pesquisas de interceptação em apps e em sites.",
"active": "Ativo",
"active_surveys": "Inquéritos ativos",
"activity": "Atividade",
"add": "Adicionar",
@@ -139,6 +140,7 @@
"app": "Aplicação",
"app_survey": "Inquérito (app)",
"apply_filters": "Aplicar filtros",
"archived": "Arquivado",
"are_you_sure": "Tem a certeza?",
"attributes": "Atributos",
"back": "Voltar",
@@ -181,6 +183,7 @@
"count_questions": "{count, plural, one {{count} pergunta} other {{count} perguntas}}",
"count_responses": "{count, plural, one {{count} resposta} other {{count} respostas}}",
"count_selections": "{count, plural, one {{count} seleção} other {{count} seleções}}",
"create": "Criar",
"create_new_organization": "Criar nova organização",
"create_segment": "Criar segmento",
"create_survey": "Criar inquérito",
@@ -1133,6 +1136,34 @@
"teams": "Equipas e Funções de Acesso (Ler, Ler e Escrever, Gerir)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Desbloqueie todo o poder do Formbricks. Grátis por 30 dias."
},
"feedback_record_directories": {
"all_workspaces_added": "Todos os espaços de trabalho adicionados a este diretório.",
"archive": "Arquivar",
"archive_directory": "Arquivar Diretório",
"archive_not_allowed": "Não tens permissão para arquivar este diretório.",
"are_you_sure_you_want_to_archive": "Tens a certeza de que queres arquivar este diretório? Os espaços de trabalho deixarão de ter acesso ao mesmo.",
"assign_workspaces_description": "Controla quais os espaços de trabalho que podem aceder a este diretório de registos de feedback.",
"create_directory": "Criar Diretório",
"create_new_directory": "Criar novo diretório de registos de feedback",
"description": "Gere diretórios de registos de feedback e as suas atribuições de espaços de trabalho.",
"directory": "diretório",
"directory_archived_successfully": "Diretório arquivado com sucesso",
"directory_created_successfully": "Diretório criado com sucesso",
"directory_id": "ID do Diretório",
"directory_name": "Nome do Diretório",
"directory_settings_description": "Gere o nome do diretório, atribuições de espaços de trabalho e muito mais.",
"directory_settings_title": "Definições de {directoryName}",
"directory_unarchived_successfully": "Diretório desarquivado com sucesso",
"directory_updated_successfully": "Diretório atualizado com sucesso",
"empty_state": "Não foram encontrados diretórios de registos de feedback. Cria um para começar.",
"enter_directory_name": "Insere o nome do diretório",
"nav_label": "Diretórios de Feedback",
"no_access": "Não tens permissão para gerir diretórios de registos de feedback.",
"please_fill_all_workspace_fields": "Por favor, preenche primeiro todos os campos da área de trabalho.",
"show_archived": "Mostrar arquivados",
"title": "Diretórios de Registos de Feedback",
"unarchive": "Desarquivar"
},
"general": {
"bulk_invite_warning_description": "No plano gratuito, todos os membros da organização são sempre atribuídos ao papel de \"Proprietário\".",
"cannot_delete_only_organization": "Esta é a sua única organização, não pode ser eliminada. Crie uma nova organização primeiro.",
+31
View File
@@ -118,6 +118,7 @@
"action": "Acțiune",
"actions": "Acțiuni",
"actions_description": "Acțiunile Cod și No-Code sunt utilizate pentru a declanșa chestionare de interceptare în aplicații și pe site-uri web.",
"active": "Activ",
"active_surveys": "Sondaje active",
"activity": "Activitate",
"add": "Adaugă",
@@ -139,6 +140,7 @@
"app": "Aplicație",
"app_survey": "Sondaj aplicație",
"apply_filters": "Aplică filtre",
"archived": "Arhivat",
"are_you_sure": "Ești sigur?",
"attributes": "Atribute",
"back": "Înapoi",
@@ -181,6 +183,7 @@
"count_questions": "{count, plural, one {# întrebare} few {# întrebări} other {# de întrebări}}",
"count_responses": "{count, plural, one {{count} răspuns} few {{count} răspunsuri} other {{count} de răspunsuri}}",
"count_selections": "{count, plural, one {{count} selecție} few {{count} selecții} other {{count} de selecții}}",
"create": "Creează",
"create_new_organization": "Creează organizație nouă",
"create_segment": "Creați segment",
"create_survey": "Creează sondaj",
@@ -1133,6 +1136,34 @@
"teams": "Echipe & Roluri de Acces (Citiți, Citiți și Scrieți, Gestionați)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Deblocați puterea completă a Formbricks. Gratuit timp de 30 de zile."
},
"feedback_record_directories": {
"all_workspaces_added": "Toate spațiile de lucru au fost adăugate la acest director.",
"archive": "Arhivează",
"archive_directory": "Arhivează directorul",
"archive_not_allowed": "Nu ai permisiunea să arhivezi acest director.",
"are_you_sure_you_want_to_archive": "Ești sigur că vrei să arhivezi acest director? Spațiile de lucru nu vor mai avea acces la el.",
"assign_workspaces_description": "Controlează care spații de lucru pot accesa acest director de înregistrări de feedback.",
"create_directory": "Creează director",
"create_new_directory": "Creează un nou director de înregistrări de feedback",
"description": "Gestionează directoarele de înregistrări de feedback și atribuirile lor la spații de lucru.",
"directory": "director",
"directory_archived_successfully": "Directorul a fost arhivat cu succes",
"directory_created_successfully": "Directorul a fost creat cu succes",
"directory_id": "ID director",
"directory_name": "Numele directorului",
"directory_settings_description": "Gestionează numele directorului, atribuirile la spații de lucru și multe altele.",
"directory_settings_title": "Setări {directoryName}",
"directory_unarchived_successfully": "Directorul a fost dezarhivat cu succes",
"directory_updated_successfully": "Directorul a fost actualizat cu succes",
"empty_state": "Nu au fost găsite directoare de înregistrări de feedback. Creează unul pentru a începe.",
"enter_directory_name": "Introdu numele directorului",
"nav_label": "Directoare de feedback",
"no_access": "Nu ai permisiunea de a gestiona directoarele de înregistrări de feedback.",
"please_fill_all_workspace_fields": "Te rugăm să completezi mai întâi toate câmpurile workspace-ului.",
"show_archived": "Afișează arhivate",
"title": "Directoare de Înregistrări Feedback",
"unarchive": "Dezarhivează"
},
"general": {
"bulk_invite_warning_description": "În planul gratuit, toți membrii organizației sunt întotdeauna alocați rolului „Proprietar”.",
"cannot_delete_only_organization": "Aceasta este singura ta organizație, nu poate fi ștearsă. Creează mai întâi o nouă organizație.",
+31
View File
@@ -118,6 +118,7 @@
"action": "Действие",
"actions": "Действия",
"actions_description": "Действия с кодом и без кода используются для запуска опросов-перехватчиков в приложениях и на сайтах.",
"active": "Активный",
"active_surveys": "Активные опросы",
"activity": "Активность",
"add": "Добавить",
@@ -139,6 +140,7 @@
"app": "Приложение",
"app_survey": "Опрос о приложении",
"apply_filters": "Применить фильтры",
"archived": "Архивный",
"are_you_sure": "Вы уверены?",
"attributes": "Атрибуты",
"back": "Назад",
@@ -181,6 +183,7 @@
"count_questions": "{count, plural, one {{count} вопрос} few {{count} вопроса} many {{count} вопросов} other {{count} вопросов}}",
"count_responses": "{count, plural, one {{count} ответ} few {{count} ответа} many {{count} ответов} other {{count} ответа}}",
"count_selections": "{count, plural, one {{count} выбор} few {{count} выбора} many {{count} выборов} other {{count} выбора}}",
"create": "Создать",
"create_new_organization": "Создать новую организацию",
"create_segment": "Создать сегмент",
"create_survey": "Создать опрос",
@@ -1133,6 +1136,34 @@
"teams": "Команды и роли доступа (чтение, чтение и запись, управление)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Откройте все возможности Formbricks. Бесплатно на 30 дней."
},
"feedback_record_directories": {
"all_workspaces_added": "Все рабочие пространства добавлены в этот каталог.",
"archive": "Архивировать",
"archive_directory": "Архивировать каталог",
"archive_not_allowed": "У тебя нет прав для архивирования этого каталога.",
"are_you_sure_you_want_to_archive": "Ты уверен, что хочешь архивировать этот каталог? Рабочие пространства больше не будут иметь к нему доступа.",
"assign_workspaces_description": "Управляй тем, какие рабочие пространства могут получить доступ к этому каталогу записей отзывов.",
"create_directory": "Создать каталог",
"create_new_directory": "Создать новый каталог записей отзывов",
"description": "Управляй каталогами записей отзывов и их назначением рабочим пространствам.",
"directory": "каталог",
"directory_archived_successfully": "Каталог успешно архивирован",
"directory_created_successfully": "Каталог успешно создан",
"directory_id": "ID каталога",
"directory_name": "Название каталога",
"directory_settings_description": "Управляй названием каталога, назначением рабочих пространств и другими параметрами.",
"directory_settings_title": "Настройки {directoryName}",
"directory_unarchived_successfully": "Каталог успешно разархивирован",
"directory_updated_successfully": "Каталог успешно обновлён",
"empty_state": "Каталоги записей отзывов не найдены. Создай один, чтобы начать.",
"enter_directory_name": "Введи название каталога",
"nav_label": "Каталоги отзывов",
"no_access": "У тебя нет прав для управления каталогами записей отзывов.",
"please_fill_all_workspace_fields": "Сначала заполни все поля рабочего пространства.",
"show_archived": "Показать архивные",
"title": "Директории записей обратной связи",
"unarchive": "Разархивировать"
},
"general": {
"bulk_invite_warning_description": "В бесплатном тарифе всем участникам организации всегда назначается роль \"Владелец\".",
"cannot_delete_only_organization": "Это ваша единственная организация, её нельзя удалить. Сначала создайте новую организацию.",
+31
View File
@@ -118,6 +118,7 @@
"action": "Åtgärd",
"actions": "Åtgärder",
"actions_description": "Kod- och No-Code-åtgärder används för att utlösa enkäter i appar och på webbplatser.",
"active": "Aktiv",
"active_surveys": "Aktiva enkäter",
"activity": "Aktivitet",
"add": "Lägg till",
@@ -139,6 +140,7 @@
"app": "App",
"app_survey": "App-enkät",
"apply_filters": "Tillämpa filter",
"archived": "Arkiverad",
"are_you_sure": "Är du säker?",
"attributes": "Attribut",
"back": "Tillbaka",
@@ -181,6 +183,7 @@
"count_questions": "{count, plural, one {{count} fråga} other {{count} frågor}}",
"count_responses": "{count, plural, one {{count} svar} other {{count} svar}}",
"count_selections": "{count, plural, one {{count} val} other {{count} val}}",
"create": "Skapa",
"create_new_organization": "Skapa ny organisation",
"create_segment": "Skapa segment",
"create_survey": "Skapa enkät",
@@ -1133,6 +1136,34 @@
"teams": "Team och åtkomstroller (Läs, Läs och skriv, Hantera)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "Lås upp Formbricks fulla kraft. Gratis i 30 dagar."
},
"feedback_record_directories": {
"all_workspaces_added": "Alla arbetsytor har lagts till i den här katalogen.",
"archive": "Arkivera",
"archive_directory": "Arkivera katalog",
"archive_not_allowed": "Du har inte behörighet att arkivera den här katalogen.",
"are_you_sure_you_want_to_archive": "Är du säker på att du vill arkivera den här katalogen? Arbetsytor kommer inte längre ha tillgång till den.",
"assign_workspaces_description": "Styr vilka arbetsytor som kan komma åt den här katalogen för feedbackposter.",
"create_directory": "Skapa katalog",
"create_new_directory": "Skapa ny katalog för feedbackposter",
"description": "Hantera kataloger för feedbackposter och deras arbetsytstilldelningar.",
"directory": "katalog",
"directory_archived_successfully": "Katalogen arkiverades",
"directory_created_successfully": "Katalogen skapades",
"directory_id": "Katalog-ID",
"directory_name": "Katalognamn",
"directory_settings_description": "Hantera katalognamn, arbetsytstilldelningar och mer.",
"directory_settings_title": "Inställningar för {directoryName}",
"directory_unarchived_successfully": "Katalogen återställdes från arkivet",
"directory_updated_successfully": "Katalogen uppdaterades",
"empty_state": "Inga kataloger för feedbackposter hittades. Skapa en för att komma igång.",
"enter_directory_name": "Ange katalognamn",
"nav_label": "Feedbackkataloger",
"no_access": "Du har inte behörighet att hantera kataloger för feedbackposter.",
"please_fill_all_workspace_fields": "Vänligen fyll i alla arbetsytefält först.",
"show_archived": "Visa arkiverade",
"title": "Feedbackkataloger",
"unarchive": "Avarkivera"
},
"general": {
"bulk_invite_warning_description": "På gratisplanen tilldelas alla organisationsmedlemmar alltid rollen \"Ägare\".",
"cannot_delete_only_organization": "Detta är din enda organisation, den kan inte tas bort. Skapa en ny organisation först.",
+31
View File
@@ -118,6 +118,7 @@
"action": "操作",
"actions": "操作",
"actions_description": "代码 和 无代码 操作 用于 触发 拦截 调查 在 应用程序 和 网站 中。",
"active": "活跃",
"active_surveys": "活跃 调查",
"activity": "活动",
"add": "添加",
@@ -139,6 +140,7 @@
"app": "应用",
"app_survey": "应用 程序 调查",
"apply_filters": "应用 筛选",
"archived": "已归档",
"are_you_sure": "你 确定 吗?",
"attributes": "属性",
"back": "返回",
@@ -181,6 +183,7 @@
"count_questions": "共{count}个问题",
"count_responses": "{count, plural, other {{count} 回复} }",
"count_selections": "{count, plural, other {已选择{count}项}}",
"create": "创建",
"create_new_organization": "创建 新的 组织",
"create_segment": "创建 细分",
"create_survey": "创建 调查",
@@ -1133,6 +1136,34 @@
"teams": "团队 & 访问 角色(读取, 读取 & 写入, 管理)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "解锁 Formbricks 的全部功能。免费使用 30 天。"
},
"feedback_record_directories": {
"all_workspaces_added": "所有工作区已添加到此目录。",
"archive": "归档",
"archive_directory": "归档目录",
"archive_not_allowed": "你无权归档此目录。",
"are_you_sure_you_want_to_archive": "确定要归档此目录吗?工作区将无法再访问它。",
"assign_workspaces_description": "控制哪些工作区可以访问此反馈记录目录。",
"create_directory": "创建目录",
"create_new_directory": "创建新的反馈记录目录",
"description": "管理反馈记录目录及其工作区分配。",
"directory": "目录",
"directory_archived_successfully": "目录已成功归档",
"directory_created_successfully": "目录已成功创建",
"directory_id": "目录 ID",
"directory_name": "目录名称",
"directory_settings_description": "管理目录名称、工作区分配等。",
"directory_settings_title": "{directoryName} 设置",
"directory_unarchived_successfully": "目录已成功取消归档",
"directory_updated_successfully": "目录已成功更新",
"empty_state": "未找到反馈记录目录。创建一个开始使用吧。",
"enter_directory_name": "输入目录名称",
"nav_label": "反馈目录",
"no_access": "你没有管理反馈记录目录的权限。",
"please_fill_all_workspace_fields": "请先填写所有工作区字段。",
"show_archived": "显示已归档",
"title": "反馈记录目录",
"unarchive": "取消归档"
},
"general": {
"bulk_invite_warning_description": "在免费计划中,所有组织成员都会被分配为 \"Owner \"角色。",
"cannot_delete_only_organization": "这是 您 唯一的 组织,不可 删除。请 先 创建一个新的 组织。",
+31
View File
@@ -118,6 +118,7 @@
"action": "操作",
"actions": "操作",
"actions_description": "代碼 和 無代碼 動作 用於 觸發 截取 調查 於 應用程式 和 網站上 。",
"active": "啟用中",
"active_surveys": "啟用中的問卷",
"activity": "活動",
"add": "新增",
@@ -139,6 +140,7 @@
"app": "應用程式",
"app_survey": "應用程式問卷",
"apply_filters": "套用篩選器",
"archived": "已封存",
"are_you_sure": "您確定嗎?",
"attributes": "屬性",
"back": "返回",
@@ -181,6 +183,7 @@
"count_questions": "{count, plural, other {{count} 個問題}}",
"count_responses": "{count, plural, other {{count} 答覆}}",
"count_selections": "{count, plural, other {{count} 個選擇}}",
"create": "建立",
"create_new_organization": "建立新組織",
"create_segment": "建立區隔",
"create_survey": "建立問卷",
@@ -1133,6 +1136,34 @@
"teams": "團隊和存取角色(讀取、讀取和寫入、管理)",
"unlock_the_full_power_of_formbricks_free_for_30_days": "免費解鎖 Formbricks 的全部功能,為期 30 天。"
},
"feedback_record_directories": {
"all_workspaces_added": "所有工作區都已加入此目錄。",
"archive": "封存",
"archive_directory": "封存目錄",
"archive_not_allowed": "您沒有權限封存此目錄。",
"are_you_sure_you_want_to_archive": "確定要封存此目錄嗎?工作區將無法再存取它。",
"assign_workspaces_description": "控制哪些工作區可以存取此意見回饋記錄目錄。",
"create_directory": "建立目錄",
"create_new_directory": "建立新的意見回饋記錄目錄",
"description": "管理意見回饋記錄目錄及其工作區配置。",
"directory": "目錄",
"directory_archived_successfully": "目錄已成功封存",
"directory_created_successfully": "目錄已成功建立",
"directory_id": "目錄 ID",
"directory_name": "目錄名稱",
"directory_settings_description": "管理目錄名稱、工作區配置等設定。",
"directory_settings_title": "{directoryName} 設定",
"directory_unarchived_successfully": "目錄已成功取消封存",
"directory_updated_successfully": "目錄已成功更新",
"empty_state": "找不到任何意見回饋記錄目錄。建立一個開始使用吧。",
"enter_directory_name": "輸入目錄名稱",
"nav_label": "意見回饋目錄",
"no_access": "您沒有權限管理意見回饋記錄目錄。",
"please_fill_all_workspace_fields": "請先填寫所有工作區欄位。",
"show_archived": "顯示已封存",
"title": "意見回饋記錄目錄",
"unarchive": "取消封存"
},
"general": {
"bulk_invite_warning_description": "在免費方案中,所有組織成員始終會被指派「擁有者」角色。",
"cannot_delete_only_organization": "這是您唯一的組織,無法刪除。請先建立新組織。",
@@ -290,6 +290,9 @@ export const withAuditLogging = <
case "quota":
targetId = auditLoggingCtx.quotaId;
break;
case "feedbackRecordDirectory":
targetId = auditLoggingCtx.feedbackRecordDirectoryId;
break;
default:
targetId = UNKNOWN_DATA;
break;
@@ -25,6 +25,7 @@ export const ZAuditTarget = z.enum([
"integration",
"file",
"quota",
"feedbackRecordDirectory",
]);
export const ZAuditAction = z.enum([
"created",
@@ -0,0 +1,161 @@
"use server";
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
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";
import {
archiveFeedbackRecordDirectory,
createFeedbackRecordDirectory,
getFeedbackRecordDirectoryDetails,
getOrganizationIdFromDirectoryId,
unarchiveFeedbackRecordDirectory,
updateFeedbackRecordDirectory,
} from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory";
import { ZFeedbackRecordDirectoryFormSchema } from "@/modules/ee/feedback-record-directory/types/feedback-record-directory";
const ZCreateFeedbackRecordDirectoryAction = z.object({
organizationId: z.cuid(),
name: z.string().trim().min(1, "Directory name is required"),
});
export const createFeedbackRecordDirectoryAction = authenticatedActionClient
.inputSchema(ZCreateFeedbackRecordDirectoryAction)
.action(
withAuditLogging("created", "feedbackRecordDirectory", async ({ ctx, parsedInput }) => {
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: parsedInput.organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
],
});
const result = await createFeedbackRecordDirectory(parsedInput.organizationId, parsedInput.name);
ctx.auditLoggingCtx.organizationId = parsedInput.organizationId;
ctx.auditLoggingCtx.feedbackRecordDirectoryId = result;
ctx.auditLoggingCtx.newObject = {
...(await getFeedbackRecordDirectoryDetails(result)),
};
return result;
})
);
const ZGetFeedbackRecordDirectoryDetailsAction = z.object({
directoryId: ZId,
});
export const getFeedbackRecordDirectoryDetailsAction = authenticatedActionClient
.inputSchema(ZGetFeedbackRecordDirectoryDetailsAction)
.action(async ({ parsedInput, ctx }) => {
const organizationId = await getOrganizationIdFromDirectoryId(parsedInput.directoryId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
],
});
return await getFeedbackRecordDirectoryDetails(parsedInput.directoryId);
});
const ZUpdateFeedbackRecordDirectoryAction = z.object({
directoryId: ZId,
data: ZFeedbackRecordDirectoryFormSchema,
});
export const updateFeedbackRecordDirectoryAction = authenticatedActionClient
.inputSchema(ZUpdateFeedbackRecordDirectoryAction)
.action(
withAuditLogging("updated", "feedbackRecordDirectory", async ({ ctx, parsedInput }) => {
const organizationId = await getOrganizationIdFromDirectoryId(parsedInput.directoryId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
],
});
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.feedbackRecordDirectoryId = parsedInput.directoryId;
const oldObject = await getFeedbackRecordDirectoryDetails(parsedInput.directoryId);
const result = await updateFeedbackRecordDirectory(parsedInput.directoryId, parsedInput.data);
ctx.auditLoggingCtx.oldObject = oldObject;
ctx.auditLoggingCtx.newObject = await getFeedbackRecordDirectoryDetails(parsedInput.directoryId);
return result;
})
);
const ZArchiveFeedbackRecordDirectoryAction = z.object({
directoryId: ZId,
});
export const archiveFeedbackRecordDirectoryAction = authenticatedActionClient
.inputSchema(ZArchiveFeedbackRecordDirectoryAction)
.action(
withAuditLogging("deleted", "feedbackRecordDirectory", async ({ ctx, parsedInput }) => {
const organizationId = await getOrganizationIdFromDirectoryId(parsedInput.directoryId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
],
});
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.feedbackRecordDirectoryId = parsedInput.directoryId;
const oldObject = await getFeedbackRecordDirectoryDetails(parsedInput.directoryId);
ctx.auditLoggingCtx.oldObject = oldObject;
return await archiveFeedbackRecordDirectory(parsedInput.directoryId);
})
);
const ZUnarchiveFeedbackRecordDirectoryAction = z.object({
directoryId: ZId,
});
export const unarchiveFeedbackRecordDirectoryAction = authenticatedActionClient
.inputSchema(ZUnarchiveFeedbackRecordDirectoryAction)
.action(
withAuditLogging("updated", "feedbackRecordDirectory", async ({ ctx, parsedInput }) => {
const organizationId = await getOrganizationIdFromDirectoryId(parsedInput.directoryId);
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
],
});
ctx.auditLoggingCtx.organizationId = organizationId;
ctx.auditLoggingCtx.feedbackRecordDirectoryId = parsedInput.directoryId;
const oldObject = await getFeedbackRecordDirectoryDetails(parsedInput.directoryId);
ctx.auditLoggingCtx.oldObject = oldObject;
const result = await unarchiveFeedbackRecordDirectory(parsedInput.directoryId);
ctx.auditLoggingCtx.newObject = await getFeedbackRecordDirectoryDetails(parsedInput.directoryId);
return result;
})
);
@@ -0,0 +1,102 @@
"use client";
import { FolderIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { createFeedbackRecordDirectoryAction } from "@/modules/ee/feedback-record-directory/actions";
import { Button } from "@/modules/ui/components/button";
import {
Dialog,
DialogBody,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/modules/ui/components/dialog";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
interface CreateFeedbackRecordDirectoryModalProps {
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
organizationId: string;
}
export const CreateFeedbackRecordDirectoryModal = ({
open,
setOpen,
organizationId,
}: CreateFeedbackRecordDirectoryModalProps) => {
const [directoryName, setDirectoryName] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const router = useRouter();
const handleCreation = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
const name = directoryName.trim();
const response = await createFeedbackRecordDirectoryAction({ name, organizationId });
if (response?.data) {
toast.success(t("environments.settings.feedback_record_directories.directory_created_successfully"));
router.refresh();
setOpen(false);
setDirectoryName("");
} else {
const errorMessage = getFormattedErrorMessage(response);
toast.error(errorMessage);
}
setIsLoading(false);
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<FolderIcon />
<DialogTitle>
{t("environments.settings.feedback_record_directories.create_new_directory")}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleCreation} className="gap-y-4 pt-4">
<DialogBody>
<div className="grid w-full gap-y-2 pb-4">
<Label htmlFor="directory-name">
{t("environments.settings.feedback_record_directories.directory_name")}
</Label>
<Input
id="directory-name"
name="directory-name"
value={directoryName}
onChange={(e) => {
setDirectoryName(e.target.value);
}}
placeholder={t("environments.settings.feedback_record_directories.enter_directory_name")}
/>
</div>
</DialogBody>
<DialogFooter>
<Button
variant="secondary"
type="button"
onClick={() => {
setOpen(false);
setDirectoryName("");
}}>
{t("common.cancel")}
</Button>
<Button disabled={!directoryName || isLoading} loading={isLoading} type="submit">
{t("common.create")}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,83 @@
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { archiveFeedbackRecordDirectoryAction } from "@/modules/ee/feedback-record-directory/actions";
import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
interface ArchiveFeedbackRecordDirectoryProps {
directoryId: string;
onArchive: () => void;
isOwnerOrManager: boolean;
}
export const ArchiveFeedbackRecordDirectory = ({
directoryId,
onArchive,
isOwnerOrManager,
}: ArchiveFeedbackRecordDirectoryProps) => {
const { t } = useTranslation();
const [isArchiveDialogOpen, setIsArchiveDialogOpen] = useState(false);
const [isArchiving, setIsArchiving] = useState(false);
const router = useRouter();
const handleArchive = async () => {
setIsArchiving(true);
const response = await archiveFeedbackRecordDirectoryAction({ directoryId });
if (response?.serverError) {
toast.error(getFormattedErrorMessage(response));
setIsArchiveDialogOpen(false);
setIsArchiving(false);
return;
}
if (response?.data) {
toast.success(t("environments.settings.feedback_record_directories.directory_archived_successfully"));
onArchive?.();
router.refresh();
} else {
toast.error(t("common.something_went_wrong_please_try_again"));
}
setIsArchiveDialogOpen(false);
setIsArchiving(false);
};
return (
<>
<div className="flex flex-row items-baseline space-x-2">
<TooltipRenderer
shouldRender={!isOwnerOrManager}
tooltipContent={t("environments.settings.feedback_record_directories.archive_not_allowed")}
className="w-auto">
<Button
variant="destructive"
type="button"
className="w-auto"
disabled={!isOwnerOrManager}
onClick={() => setIsArchiveDialogOpen(true)}>
{t("environments.settings.feedback_record_directories.archive_directory")}
</Button>
</TooltipRenderer>
</div>
{isArchiveDialogOpen && (
<DeleteDialog
open={isArchiveDialogOpen}
setOpen={setIsArchiveDialogOpen}
deleteWhat={t("environments.settings.feedback_record_directories.directory")}
text={t("environments.settings.feedback_record_directories.are_you_sure_you_want_to_archive")}
onDelete={handleArchive}
isDeleting={isArchiving}
title={t("environments.settings.feedback_record_directories.archive_directory")}
buttonLabel={t("environments.settings.feedback_record_directories.archive")}
/>
)}
</>
);
};
@@ -0,0 +1,295 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon, Trash2Icon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useMemo } from "react";
import { FormProvider, SubmitHandler, useForm, useWatch } from "react-hook-form";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TOrganizationRole } from "@formbricks/types/memberships";
import { getAccessFlags } from "@/lib/membership/utils";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { updateFeedbackRecordDirectoryAction } from "@/modules/ee/feedback-record-directory/actions";
import { ArchiveFeedbackRecordDirectory } from "@/modules/ee/feedback-record-directory/components/feedback-record-directory-settings/archive-feedback-record-directory";
import {
TFeedbackRecordDirectoryDetails,
TFeedbackRecordDirectoryFormSchema,
ZFeedbackRecordDirectoryFormSchema,
} from "@/modules/ee/feedback-record-directory/types/feedback-record-directory";
import { TOrganizationProject } from "@/modules/ee/teams/team-list/types/project";
import { Button } from "@/modules/ui/components/button";
import {
Dialog,
DialogBody,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/modules/ui/components/dialog";
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
import { IdBadge } from "@/modules/ui/components/id-badge";
import { Input } from "@/modules/ui/components/input";
import { InputCombobox } from "@/modules/ui/components/input-combo-box";
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
import { Muted } from "@/modules/ui/components/typography";
interface FeedbackRecordDirectorySettingsModalProps {
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
directory: TFeedbackRecordDirectoryDetails;
orgProjects: TOrganizationProject[];
membershipRole?: TOrganizationRole;
}
export const FeedbackRecordDirectorySettingsModal = ({
open,
setOpen,
directory,
orgProjects,
membershipRole,
}: FeedbackRecordDirectorySettingsModalProps) => {
const { t } = useTranslation();
const { isOwner, isManager } = getAccessFlags(membershipRole);
const isOwnerOrManager = isOwner || isManager;
const router = useRouter();
const initialProjectIds = useMemo(() => {
return new Set(directory.projects.map((project) => project.projectId));
}, [directory.projects]);
const initialProjects = useMemo(() => {
const projects = directory.projects.map((project) => ({
projectId: project.projectId,
}));
return projects.length ? projects : [{ projectId: "" }];
}, [directory.projects]);
const form = useForm<TFeedbackRecordDirectoryFormSchema>({
defaultValues: {
name: directory.name,
projects: initialProjects,
},
mode: "onChange",
resolver: zodResolver(ZFeedbackRecordDirectoryFormSchema),
});
const {
control,
handleSubmit,
formState: { isSubmitting },
setValue,
} = form;
const closeSettingsModal = () => {
setOpen(false);
};
const handleUpdate: SubmitHandler<TFeedbackRecordDirectoryFormSchema> = async (data) => {
const projects = data.projects.filter((p) => p.projectId);
const response = await updateFeedbackRecordDirectoryAction({
directoryId: directory.id,
data: {
name: data.name,
projects,
},
});
if (response?.data) {
toast.success(t("environments.settings.feedback_record_directories.directory_updated_successfully"));
closeSettingsModal();
router.refresh();
} else {
const errorMessage = getFormattedErrorMessage(response);
toast.error(errorMessage);
}
};
const watchProjects = useWatch({ control, name: "projects" }) || [];
const handleAddProject = () => {
const newProjects = [...watchProjects, { projectId: "" }];
setValue("projects", newProjects);
};
const handleRemoveProject = (index: number) => {
setValue(
"projects",
watchProjects.filter((_, i) => i !== index)
);
};
const selectedProjectIds = watchProjects.map((p) => p.projectId);
const getProjectOptionsForIndex = (index: number) => {
const currentProjectId = watchProjects[index]?.projectId;
return orgProjects
.filter((op) => !selectedProjectIds.includes(op?.id) || op?.id === currentProjectId)
.map((op) => ({ label: op?.name ?? "", value: op?.id }))
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
};
const hasEmptyProject = watchProjects.some((p) => !p.projectId);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader className="pb-4">
<DialogTitle>
{t("environments.settings.feedback_record_directories.directory_settings_title", {
directoryName: directory.name,
})}
</DialogTitle>
<DialogDescription>
{t("environments.settings.feedback_record_directories.directory_settings_description")}
</DialogDescription>
</DialogHeader>
<FormProvider {...form}>
<form className="contents space-y-4" onSubmit={handleSubmit(handleUpdate)}>
<DialogBody className="flex-grow space-y-6 overflow-y-auto">
<FormField
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormItem>
<FormLabel>
{t("environments.settings.feedback_record_directories.directory_name")}
</FormLabel>
<FormControl>
<Input
type="text"
placeholder={t("environments.settings.feedback_record_directories.directory_name")}
{...field}
disabled={!isOwnerOrManager}
/>
</FormControl>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</FormItem>
)}
/>
<IdBadge
id={directory.id}
label={t("environments.settings.feedback_record_directories.directory_id")}
variant="column"
/>
{/* Workspace Assignments Section */}
<div className="space-y-2">
<div className="flex flex-col space-y-1">
<FormLabel>{t("common.workspaces")}</FormLabel>
<Muted className="block text-slate-500">
{t("environments.settings.feedback_record_directories.assign_workspaces_description")}
</Muted>
</div>
<FormField
control={control}
name="projects"
render={({ fieldState: { error } }) => (
<FormItem className="flex-1">
<div className="space-y-2">
{watchProjects.map((project, index) => {
const isExistingProject =
project.projectId && initialProjectIds.has(project.projectId);
const isSelectDisabled = isExistingProject || !isOwnerOrManager;
return (
<div key={`project-${project.projectId}-${index}`} className="flex gap-2.5">
<FormField
control={control}
name={`projects.${index}.projectId`}
render={({ field, fieldState: { error: fieldError } }) => (
<FormItem className="flex-1">
<div
className={
isSelectDisabled ? "pointer-events-none opacity-50" : undefined
}>
<InputCombobox
id={`project-select-${index}`}
options={getProjectOptionsForIndex(index)}
value={field.value || null}
onChangeValue={(val) => {
const value = typeof val === "string" ? val : "";
field.onChange(value);
}}
showSearch
searchPlaceholder={t("common.search")}
comboboxClasses="flex-1 min-w-0 w-full"
emptyDropdownText={t("environments.surveys.edit.no_option_found")}
/>
</div>
{fieldError?.message && (
<FormError className="text-left">{fieldError.message}</FormError>
)}
</FormItem>
)}
/>
{watchProjects.length > 1 && (
<Button
size="icon"
type="button"
variant="secondary"
className="shrink-0"
disabled={!isOwnerOrManager}
onClick={() => handleRemoveProject(index)}>
<Trash2Icon className="h-4 w-4" />
</Button>
)}
</div>
);
})}
</div>
{error?.root?.message && (
<FormError className="text-left">{error.root.message}</FormError>
)}
</FormItem>
)}
/>
<TooltipRenderer
shouldRender={selectedProjectIds.length === orgProjects.length || hasEmptyProject}
triggerClass="inline-block"
tooltipContent={
hasEmptyProject
? t(
"environments.settings.feedback_record_directories.please_fill_all_workspace_fields"
)
: t("environments.settings.feedback_record_directories.all_workspaces_added")
}>
<Button
size="sm"
type="button"
variant="secondary"
onClick={handleAddProject}
disabled={
!isOwnerOrManager || selectedProjectIds.length === orgProjects.length || hasEmptyProject
}>
<PlusIcon className="h-4 w-4" />
{t("common.add_workspace")}
</Button>
</TooltipRenderer>
</div>
</DialogBody>
<DialogFooter>
<div className="w-full">
<ArchiveFeedbackRecordDirectory
directoryId={directory.id}
onArchive={closeSettingsModal}
isOwnerOrManager={isOwnerOrManager}
/>
</div>
<Button size="default" type="button" variant="outline" onClick={closeSettingsModal}>
{t("common.cancel")}
</Button>
<Button type="submit" size="default" loading={isSubmitting} disabled={!isOwnerOrManager}>
{t("common.save")}
</Button>
</DialogFooter>
</form>
</FormProvider>
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,161 @@
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { TOrganizationRole } from "@formbricks/types/memberships";
import { getAccessFlags } from "@/lib/membership/utils";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import {
getFeedbackRecordDirectoryDetailsAction,
unarchiveFeedbackRecordDirectoryAction,
} from "@/modules/ee/feedback-record-directory/actions";
import { CreateFeedbackRecordDirectoryModal } from "@/modules/ee/feedback-record-directory/components/create-feedback-record-directory-modal";
import { FeedbackRecordDirectorySettingsModal } from "@/modules/ee/feedback-record-directory/components/feedback-record-directory-settings/feedback-record-directory-settings-modal";
import {
TFeedbackRecordDirectory,
TFeedbackRecordDirectoryDetails,
} from "@/modules/ee/feedback-record-directory/types/feedback-record-directory";
import { TOrganizationProject } from "@/modules/ee/teams/team-list/types/project";
import { Badge } from "@/modules/ui/components/badge";
import { Button } from "@/modules/ui/components/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/modules/ui/components/table";
interface FeedbackRecordDirectoryTableProps {
directories: TFeedbackRecordDirectory[];
organizationId: string;
orgProjects: TOrganizationProject[];
membershipRole?: TOrganizationRole;
}
export const FeedbackRecordDirectoryTable = ({
directories,
organizationId,
orgProjects,
membershipRole,
}: FeedbackRecordDirectoryTableProps) => {
const { t } = useTranslation();
const [openCreateModal, setOpenCreateModal] = useState(false);
const [openSettingsModal, setOpenSettingsModal] = useState(false);
const [selectedDirectory, setSelectedDirectory] = useState<TFeedbackRecordDirectoryDetails>();
const [showArchived, setShowArchived] = useState(false);
const router = useRouter();
const { isOwner, isManager } = getAccessFlags(membershipRole);
const isOwnerOrManager = isOwner || isManager;
const handleManageDirectory = async (directoryId: string) => {
const response = await getFeedbackRecordDirectoryDetailsAction({ directoryId });
if (response?.data) {
setSelectedDirectory(response.data);
setOpenSettingsModal(true);
} else {
const errorMessage = getFormattedErrorMessage(response);
toast.error(errorMessage);
}
};
const handleUnarchiveDirectory = async (directoryId: string) => {
const response = await unarchiveFeedbackRecordDirectoryAction({ directoryId });
if (response?.data) {
toast.success(t("environments.settings.feedback_record_directories.directory_unarchived_successfully"));
router.refresh();
} else {
const errorMessage = getFormattedErrorMessage(response);
toast.error(errorMessage);
}
};
const filteredDirectories = showArchived ? directories : directories.filter((d) => !d.isArchived);
return (
<>
{isOwnerOrManager && (
<div className="mb-4 flex items-center justify-between">
<label className="flex items-center gap-2 text-sm text-slate-500">
<input
type="checkbox"
checked={showArchived}
onChange={(e) => setShowArchived(e.target.checked)}
className="rounded border-slate-300"
/>
{t("environments.settings.feedback_record_directories.show_archived")}
</label>
<Button size="sm" onClick={() => setOpenCreateModal(true)}>
{t("environments.settings.feedback_record_directories.create_directory")}
</Button>
</div>
)}
<div className="overflow-hidden rounded-lg" aria-label="Feedback record directories list">
<Table>
<TableHeader role="rowgroup">
<TableRow className="bg-slate-100" role="row">
<TableHead className="font-medium text-slate-500">
{t("environments.settings.feedback_record_directories.directory_name")}
</TableHead>
<TableHead className="font-medium text-slate-500">{t("common.workspaces")}</TableHead>
<TableHead className="font-medium text-slate-500">{t("common.status")}</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody className="[&_tr:last-child]:border-b">
{filteredDirectories.length === 0 && (
<TableRow>
<TableCell colSpan={4} className="text-center hover:bg-transparent">
{t("environments.settings.feedback_record_directories.empty_state")}
</TableCell>
</TableRow>
)}
{filteredDirectories.map((directory) => (
<TableRow key={directory.id} className="hover:bg-transparent">
<TableCell>{directory.name}</TableCell>
<TableCell>{directory.projectCount}</TableCell>
<TableCell>
{directory.isArchived ? (
<Badge type="gray" size="tiny" text={t("common.archived")} />
) : (
<Badge type="success" size="tiny" text={t("common.active")} />
)}
</TableCell>
<TableCell className="flex justify-end gap-2">
{isOwnerOrManager && !directory.isArchived && (
<Button size="sm" variant="secondary" onClick={() => handleManageDirectory(directory.id)}>
{t("common.manage")}
</Button>
)}
{isOwnerOrManager && directory.isArchived && (
<Button
size="sm"
variant="secondary"
onClick={() => handleUnarchiveDirectory(directory.id)}>
{t("environments.settings.feedback_record_directories.unarchive")}
</Button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<CreateFeedbackRecordDirectoryModal
open={openCreateModal}
setOpen={setOpenCreateModal}
organizationId={organizationId}
/>
{openSettingsModal && selectedDirectory && (
<FeedbackRecordDirectorySettingsModal
open={openSettingsModal}
setOpen={setOpenSettingsModal}
directory={selectedDirectory}
orgProjects={orgProjects}
membershipRole={membershipRole}
/>
)}
</>
);
};
@@ -0,0 +1,36 @@
import { TOrganizationRole } from "@formbricks/types/memberships";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { getTranslate } from "@/lingodotdev/server";
import { FeedbackRecordDirectoryTable } from "@/modules/ee/feedback-record-directory/components/feedback-record-directory-table";
import { getFeedbackRecordDirectories } from "@/modules/ee/feedback-record-directory/lib/feedback-record-directory";
import { getProjectsByOrganizationId } from "@/modules/ee/teams/team-list/lib/project";
interface FeedbackRecordDirectoryViewProps {
organizationId: string;
membershipRole?: TOrganizationRole;
}
export const FeedbackRecordDirectoryView = async ({
organizationId,
membershipRole,
}: FeedbackRecordDirectoryViewProps) => {
const t = await getTranslate();
const [directories, orgProjects] = await Promise.all([
getFeedbackRecordDirectories(organizationId),
getProjectsByOrganizationId(organizationId),
]);
return (
<SettingsCard
title={t("environments.settings.feedback_record_directories.title")}
description={t("environments.settings.feedback_record_directories.description")}>
<FeedbackRecordDirectoryTable
directories={directories}
organizationId={organizationId}
orgProjects={orgProjects}
membershipRole={membershipRole}
/>
</SettingsCard>
);
};
@@ -0,0 +1,266 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { z } from "zod";
import { prisma } from "@formbricks/database";
import { ZId } from "@formbricks/types/common";
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
import { validateInputs } from "@/lib/utils/validate";
import {
TFeedbackRecordDirectory,
TFeedbackRecordDirectoryDetails,
TFeedbackRecordDirectoryFormSchema,
ZFeedbackRecordDirectoryFormSchema,
} from "@/modules/ee/feedback-record-directory/types/feedback-record-directory";
export const getFeedbackRecordDirectories = reactCache(
async (organizationId: string): Promise<TFeedbackRecordDirectory[]> => {
validateInputs([organizationId, ZId]);
try {
const directories = await prisma.feedbackRecordDirectory.findMany({
where: {
organizationId,
},
select: {
id: true,
name: true,
isArchived: true,
_count: {
select: {
projects: true,
},
},
},
orderBy: {
createdAt: "desc",
},
});
return directories.map((dir) => ({
id: dir.id,
name: dir.name,
isArchived: dir.isArchived,
projectCount: dir._count.projects,
}));
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
}
);
export const getFeedbackRecordDirectoryDetails = reactCache(
async (directoryId: string): Promise<TFeedbackRecordDirectoryDetails | null> => {
validateInputs([directoryId, ZId]);
try {
const directory = await prisma.feedbackRecordDirectory.findUnique({
where: {
id: directoryId,
},
select: {
id: true,
name: true,
isArchived: true,
organizationId: true,
projects: {
select: {
projectId: true,
project: {
select: {
name: true,
},
},
},
},
},
});
if (!directory) {
return null;
}
return {
id: directory.id,
name: directory.name,
isArchived: directory.isArchived,
organizationId: directory.organizationId,
projects: directory.projects.map((dp) => ({
projectId: dp.projectId,
projectName: dp.project.name,
})),
};
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
}
);
export const createFeedbackRecordDirectory = async (
organizationId: string,
name: string
): Promise<string> => {
validateInputs([organizationId, ZId], [name, z.string()]);
try {
const existingDirectory = await prisma.feedbackRecordDirectory.findFirst({
where: {
name,
organizationId,
},
});
if (existingDirectory) {
throw new InvalidInputError("A feedback record directory with this name already exists");
}
if (name.trim().length < 1) {
throw new InvalidInputError("Directory name must be at least 1 character long");
}
const directory = await prisma.feedbackRecordDirectory.create({
data: {
name,
organizationId,
},
select: {
id: true,
},
});
return directory.id;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
export const updateFeedbackRecordDirectory = async (
directoryId: string,
data: TFeedbackRecordDirectoryFormSchema
): Promise<boolean> => {
validateInputs([directoryId, ZId], [data, ZFeedbackRecordDirectoryFormSchema]);
try {
const { name, projects } = data;
const directory = await prisma.feedbackRecordDirectory.findUnique({
where: { id: directoryId },
});
if (!directory) {
throw new ResourceNotFoundError("FeedbackRecordDirectory", directoryId);
}
const currentDetails = await getFeedbackRecordDirectoryDetails(directoryId);
if (!currentDetails) {
throw new ResourceNotFoundError("FeedbackRecordDirectory", directoryId);
}
// Validate that all specified projects belong to the same organization
const projectIds = projects.map((p) => p.projectId);
if (projectIds.length > 0) {
const orgProjectsCount = await prisma.project.count({
where: {
id: { in: projectIds },
organizationId: directory.organizationId,
},
});
if (orgProjectsCount !== projectIds.length) {
throw new InvalidInputError("Some specified projects do not belong to the organization.");
}
}
// Determine deleted projects (in current but not in new)
const deletedProjects: string[] = [];
for (const cp of currentDetails.projects) {
if (!projects.some((p) => p.projectId === cp.projectId)) {
deletedProjects.push(cp.projectId);
}
}
const payload: Prisma.FeedbackRecordDirectoryUpdateInput = {
name: currentDetails.name !== name ? name : undefined,
projects: {
deleteMany: {
projectId: { in: deletedProjects },
},
upsert: projects.map((p) => ({
where: {
feedbackRecordDirectoryId_projectId: {
feedbackRecordDirectoryId: directoryId,
projectId: p.projectId,
},
},
update: {},
create: { projectId: p.projectId },
})),
},
};
await prisma.feedbackRecordDirectory.update({
where: { id: directoryId },
data: payload,
});
return true;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
export const archiveFeedbackRecordDirectory = async (directoryId: string): Promise<boolean> => {
validateInputs([directoryId, ZId]);
try {
await prisma.feedbackRecordDirectory.update({
where: { id: directoryId },
data: { isArchived: true },
});
return true;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
export const unarchiveFeedbackRecordDirectory = async (directoryId: string): Promise<boolean> => {
validateInputs([directoryId, ZId]);
try {
await prisma.feedbackRecordDirectory.update({
where: { id: directoryId },
data: { isArchived: false },
});
return true;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
export const getOrganizationIdFromDirectoryId = async (directoryId: string): Promise<string> => {
validateInputs([directoryId, ZId]);
const directory = await prisma.feedbackRecordDirectory.findUnique({
where: { id: directoryId },
select: { organizationId: true },
});
if (!directory) {
throw new ResourceNotFoundError("FeedbackRecordDirectory", directoryId);
}
return directory.organizationId;
};
@@ -0,0 +1,54 @@
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
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 { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
export const FeedbackRecordDirectoriesPage = async (props: {
params: Promise<{ environmentId: string }>;
}) => {
const params = await props.params;
const t = await getTranslate();
const { currentUserMembership, organization } = await getEnvironmentAuth(params.environmentId);
const { isOwner, isManager } = getAccessFlags(currentUserMembership?.role);
if (!isOwner && !isManager) {
return (
<PageContentWrapper>
<PageHeader pageTitle={t("environments.settings.general.organization_settings")}>
<OrganizationSettingsNavbar
environmentId={params.environmentId}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
membershipRole={currentUserMembership?.role}
activeId="feedback-record-directories"
/>
</PageHeader>
<p className="text-sm text-slate-500">
{t("environments.settings.feedback_record_directories.no_access")}
</p>
</PageContentWrapper>
);
}
return (
<PageContentWrapper>
<PageHeader pageTitle={t("environments.settings.general.organization_settings")}>
<OrganizationSettingsNavbar
environmentId={params.environmentId}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
membershipRole={currentUserMembership?.role}
activeId="feedback-record-directories"
/>
</PageHeader>
<FeedbackRecordDirectoryView
organizationId={organization.id}
membershipRole={currentUserMembership?.role}
/>
</PageContentWrapper>
);
};
@@ -0,0 +1,37 @@
import { z } from "zod";
import { ZId } from "@formbricks/types/common";
export const ZFeedbackRecordDirectory = z.object({
id: ZId,
name: z.string(),
isArchived: z.boolean(),
projectCount: z.number(),
});
export type TFeedbackRecordDirectory = z.infer<typeof ZFeedbackRecordDirectory>;
export const ZFeedbackRecordDirectoryDetails = z.object({
id: ZId,
name: z.string(),
isArchived: z.boolean(),
organizationId: ZId,
projects: z.array(
z.object({
projectId: ZId,
projectName: z.string(),
})
),
});
export type TFeedbackRecordDirectoryDetails = z.infer<typeof ZFeedbackRecordDirectoryDetails>;
export const ZFeedbackRecordDirectoryFormSchema = z.object({
name: z.string().trim().min(1, "Directory name is required"),
projects: z.array(
z.object({
projectId: z.string().trim().min(1, "Please select a workspace"),
})
),
});
export type TFeedbackRecordDirectoryFormSchema = z.infer<typeof ZFeedbackRecordDirectoryFormSchema>;
@@ -25,6 +25,8 @@ interface DeleteDialogProps {
onSave?: () => void;
children?: React.ReactNode;
disabled?: boolean;
title?: string;
buttonLabel?: string;
}
export const DeleteDialog = ({
@@ -39,6 +41,8 @@ export const DeleteDialog = ({
onSave,
children,
disabled,
title,
buttonLabel,
}: DeleteDialogProps) => {
const { t } = useTranslation();
return (
@@ -48,7 +52,7 @@ export const DeleteDialog = ({
<div className="flex items-center gap-2">
<CircleAlert className="h-4 w-4" />
<div>
<DialogTitle>{t("common.delete_what", { deleteWhat })}</DialogTitle>
<DialogTitle>{title || t("common.delete_what", { deleteWhat })}</DialogTitle>
<DialogDescription>
{t("environments.workspace.general.this_action_cannot_be_undone")}
</DialogDescription>
@@ -80,7 +84,7 @@ export const DeleteDialog = ({
loading={isDeleting}
disabled={disabled || isDeleting || isSaving}>
<TrashIcon />
{t("common.delete")}
{buttonLabel || t("common.delete")}
</Button>
</DialogFooter>
</DialogContent>
@@ -0,0 +1,36 @@
-- CreateTable
CREATE TABLE "FeedbackRecordDirectory" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"name" TEXT NOT NULL,
"isArchived" BOOLEAN NOT NULL DEFAULT false,
"organizationId" TEXT NOT NULL,
CONSTRAINT "FeedbackRecordDirectory_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "FeedbackRecordDirectoryProject" (
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"feedbackRecordDirectoryId" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
CONSTRAINT "FeedbackRecordDirectoryProject_pkey" PRIMARY KEY ("feedbackRecordDirectoryId","projectId")
);
-- CreateIndex
CREATE UNIQUE INDEX "FeedbackRecordDirectory_organizationId_name_key" ON "FeedbackRecordDirectory"("organizationId", "name");
-- CreateIndex
CREATE INDEX "FeedbackRecordDirectoryProject_projectId_idx" ON "FeedbackRecordDirectoryProject"("projectId");
-- AddForeignKey
ALTER TABLE "FeedbackRecordDirectory" ADD CONSTRAINT "FeedbackRecordDirectory_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FeedbackRecordDirectoryProject" ADD CONSTRAINT "FeedbackRecordDirectoryProject_feedbackRecordDirectoryId_fkey" FOREIGN KEY ("feedbackRecordDirectoryId") REFERENCES "FeedbackRecordDirectory"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FeedbackRecordDirectoryProject" ADD CONSTRAINT "FeedbackRecordDirectoryProject_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+71 -31
View File
@@ -625,28 +625,29 @@ enum SurveyOverlay {
/// @property recontactDays - Default recontact delay for surveys
/// @property placement - Default widget placement for in-app surveys
model Project {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
name String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
organizationId String
environments Environment[]
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
name String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
organizationId String
environments Environment[]
/// [Styling]
styling Json @default("{\"allowStyleOverwrite\":true}")
styling Json @default("{\"allowStyleOverwrite\":true}")
/// [ProjectConfig]
config Json @default("{}")
recontactDays Int @default(7)
linkSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in link surveys
inAppSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in in-app surveys
placement WidgetPlacement @default(bottomRight)
clickOutsideClose Boolean @default(true)
overlay SurveyOverlay @default(none)
languages Language[]
config Json @default("{}")
recontactDays Int @default(7)
linkSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in link surveys
inAppSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in in-app surveys
placement WidgetPlacement @default(bottomRight)
clickOutsideClose Boolean @default(true)
overlay SurveyOverlay @default(none)
languages Language[]
/// [Logo]
logo Json?
projectTeams ProjectTeam[]
customHeadScripts String? // Custom HTML scripts for link surveys (self-hosted only)
logo Json?
projectTeams ProjectTeam[]
customHeadScripts String? // Custom HTML scripts for link surveys (self-hosted only)
feedbackRecordDirectoryProjects FeedbackRecordDirectoryProject[]
@@unique([organizationId, name])
}
@@ -663,19 +664,20 @@ model Project {
/// @property whitelabel - Whitelabel configuration for the organization
/// @property isAIEnabled - Controls access to AI-powered features
model Organization {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
name String
memberships Membership[]
projects Project[]
billing OrganizationBilling?
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
name String
memberships Membership[]
projects Project[]
billing OrganizationBilling?
/// [OrganizationWhitelabel]
whitelabel Json @default("{}")
invites Invite[]
isAIEnabled Boolean @default(false)
teams Team[]
apiKeys ApiKey[]
whitelabel Json @default("{}")
invites Invite[]
isAIEnabled Boolean @default(false)
teams Team[]
apiKeys ApiKey[]
feedbackRecordDirectories FeedbackRecordDirectory[]
}
/// Stores billing and Stripe synchronization data for an organization.
@@ -1023,3 +1025,41 @@ model ProjectTeam {
@@id([projectId, teamId])
@@index([teamId])
}
/// Represents a feedback record directory (Hub tenant) owned by an organization.
/// Directories group feedback data and are assigned to workspaces for access control.
///
/// @property id - Unique identifier for the directory
/// @property name - Display name of the directory
/// @property isArchived - Soft delete flag
/// @property organization - The parent organization
/// @property projects - Workspaces assigned to this directory
model FeedbackRecordDirectory {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
name String
isArchived Boolean @default(false)
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
projects FeedbackRecordDirectoryProject[]
@@unique([organizationId, name])
}
/// Links feedback record directories to projects (workspaces).
/// Manages which workspaces can access a given directory.
///
/// @property feedbackRecordDirectory - The directory being accessed
/// @property project - The workspace receiving access
model FeedbackRecordDirectoryProject {
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
feedbackRecordDirectoryId String
feedbackRecordDirectory FeedbackRecordDirectory @relation(fields: [feedbackRecordDirectoryId], references: [id], onDelete: Cascade)
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@id([feedbackRecordDirectoryId, projectId])
@@index([projectId])
}
+2
View File
@@ -10,6 +10,7 @@ checksums:
common/finish: ffa7a10f71182b48fefed7135bee24fa
common/language_switch: fd72a9ada13f672f4fd5da863b22cc46
common/next: 89ddbcf710eba274963494f312bdc8a9
common/no_results_found: 5518f2865757dc73900aa03ef8be6934
common/open_in_new_tab: 6844e4922a7a40a7ee25c10ea109cdeb
common/people_responded: b685fb877090d8658db724ad07a0dbd8
common/please_retry_now_or_try_again_later: 949a3841e2eb01fa249790a42bf23aa5
@@ -22,6 +23,7 @@ checksums:
common/respondents_will_not_see_this_card: 18c3dd44d6ff6ca2310ad196b84f30d3
common/retry: 6e44d18639560596569a1278f9c83676
common/retrying: 40989361ea5f6b95897b95ac928b5bd9
common/search: fe877a75eac472fc5b188c135c78a558
common/select_option: d68a0fb9afd0817dc31b3e9cb11855cb
common/select_options: d5a80087e889848e0fed3f1be359366f
common/sending_responses: 244f1aebc3f6a101ae2f8b630d7967ec