From 2d7b99ba262bc6f547c5c4e37f77e1ab26318c1a Mon Sep 17 00:00:00 2001
From: Johannes <72809645+jobenjada@users.noreply.github.com>
Date: Thu, 11 Dec 2025 21:01:48 -0800
Subject: [PATCH] feat: allow team admins to invite members to their own teams
(#6891)
Co-authored-by: pandeymangg
---
.../components/organization-breadcrumb.tsx | 2 +-
.../components/OrganizationSettingsNavbar.tsx | 2 +-
apps/web/i18n.lock | 10 +-
apps/web/locales/de-DE.json | 10 +-
apps/web/locales/en-US.json | 13 +-
apps/web/locales/es-ES.json | 10 +-
apps/web/locales/fr-FR.json | 10 +-
apps/web/locales/ja-JP.json | 10 +-
apps/web/locales/nl-NL.json | 10 +-
apps/web/locales/pt-BR.json | 10 +-
apps/web/locales/pt-PT.json | 10 +-
apps/web/locales/ro-RO.json | 10 +-
apps/web/locales/sv-SE.json | 8 +-
apps/web/locales/zh-Hans-CN.json | 10 +-
apps/web/locales/zh-Hant-TW.json | 10 +-
.../auth/signup/lib/__tests__/team.test.ts | 66 ++++++-
apps/web/modules/auth/signup/lib/team.ts | 26 +--
.../components/add-member-role.tsx | 2 +-
apps/web/modules/ee/teams/lib/roles.test.ts | 50 +++++-
apps/web/modules/ee/teams/lib/roles.ts | 28 +++
.../project-teams/components/access-table.tsx | 2 +-
.../project-teams/components/access-view.tsx | 5 +-
.../project-teams/components/manage-team.tsx | 21 +--
.../modules/ee/teams/project-teams/page.tsx | 6 +-
.../team-settings/team-settings-modal.tsx | 167 +++++++++++-------
.../organization/settings/teams/actions.ts | 90 ++++++++--
.../edit-memberships/organization-actions.tsx | 27 ++-
.../invite-member/individual-invite-tab.tsx | 102 +++++++----
.../invite-member/invite-member-modal.tsx | 38 ++--
.../teams/components/members-view.tsx | 7 +
.../organization/settings/teams/page.tsx | 13 +-
.../link/components/link-survey-wrapper.tsx | 4 +-
.../ui/components/client-logo/index.tsx | 7 +-
.../modules/ui/components/select/index.tsx | 2 +-
apps/web/playwright/organization.spec.ts | 6 +-
pnpm-lock.yaml | 1 +
36 files changed, 588 insertions(+), 217 deletions(-)
diff --git a/apps/web/app/(app)/environments/[environmentId]/components/organization-breadcrumb.tsx b/apps/web/app/(app)/environments/[environmentId]/components/organization-breadcrumb.tsx
index dd7fd851bb..64cffa3d19 100644
--- a/apps/web/app/(app)/environments/[environmentId]/components/organization-breadcrumb.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/components/organization-breadcrumb.tsx
@@ -135,7 +135,7 @@ export const OrganizationBreadcrumb = ({
},
{
id: "teams",
- label: t("common.teams"),
+ label: t("common.members_and_teams"),
href: `/environments/${currentEnvironmentId}/settings/teams`,
},
{
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar.tsx
index 65a8ca0429..f43b9014ad 100644
--- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar.tsx
@@ -36,7 +36,7 @@ export const OrganizationSettingsNavbar = ({
},
{
id: "teams",
- label: t("common.teams"),
+ label: t("common.members_and_teams"),
href: `/environments/${environmentId}/settings/teams`,
current: pathname?.includes("/teams"),
},
diff --git a/apps/web/i18n.lock b/apps/web/i18n.lock
index e98cff6619..46a9532cff 100644
--- a/apps/web/i18n.lock
+++ b/apps/web/i18n.lock
@@ -234,6 +234,7 @@ checksums:
common/maximum: 4c07541dd1f093775bdc61b559cca6c8
common/member: 1606dc30b369856b9dba1fe9aec425d2
common/members: 0932e80cba1e3e0a7f52bb67ff31da32
+ common/members_and_teams: bf5c3fadcb9fc23533ec1532b805ac08
common/membership_not_found: 7ac63584af23396aace9992ad919ffd4
common/metadata: 695d4f7da261ba76e3be4de495491028
common/minimum: d9759235086d0169928b3c1401115e22
@@ -313,6 +314,7 @@ checksums:
common/read_docs: 426ba960bfedf186a878b7467867f9d2
common/recipients: f90e7f266be3f5a724858f21a9fd855e
common/remove: dba2fe5fe9f83f8078c687f28cba4b52
+ common/remove_from_team: 69bcc7a1001c3017f9de578ee22cffd6
common/reorder_and_hide_columns: a5e3d7c0c7ef879211d05a37be1c5069
common/report_survey: 147dd05db52e35f5d1f837460fb720f5
common/request_pricing: 58eb24af4f098632709cb7482b70a1cb
@@ -380,7 +382,8 @@ checksums:
common/team_access: 45c6232c71b760eaa33b932dabab4c1c
common/team_id: 134e32d6f7184577a46b2fd83e85e532
common/team_name: 549d949de4b9adad4afd6427a60a329e
- common/teams: a2fbdec69342366a2b6033d119aa279a
+ common/team_role: 66db395781aef64ef3791417b3b67c0b
+ common/teams: b63448c05270497973ac4407047dae02
common/teams_not_found: 02f333a64a83c1c014d8900ec9666345
common/text: 4ddccc1974775ed7357f9beaf9361cec
common/time: b504a03d52e8001bfdc5cb6205364f42
@@ -840,7 +843,6 @@ checksums:
environments/project/tags/tags_merged: 544471de666f93fbb0ab600321d1e553
environments/project/teams/manage_teams: d7b5f26335cea450c333832adbe0b6ad
environments/project/teams/no_teams_found: fb6680d4b5b73731697b100713afb50d
- environments/project/teams/only_organization_owners_and_managers_can_manage_teams: 179056fade669d34f63fb1ee965b8024
environments/project/teams/permission: cc2ed7274bd8267f9e0a10b079584d8b
environments/project/teams/team_name: d1a5f99dbf503ca53f06b3a98b511d02
environments/project/teams/team_settings_description: da32d77993f5c5c7547cdf3e1d3fc7d5
@@ -1086,13 +1088,17 @@ checksums:
environments/settings/teams/manage_team: 4c52e636cfd1451a08179fb7a68042ab
environments/settings/teams/manage_team_disabled: 2aaa0557b403a5bc657ec9e8b19ac5ac
environments/settings/teams/manager_role_description: 39846863fa85ff8b1c6e4f354eb5018f
+ environments/settings/teams/member: 1606dc30b369856b9dba1fe9aec425d2
environments/settings/teams/member_role_description: 1c5deaece65798b74cc0d34525506c18
environments/settings/teams/member_role_info_message: 0a276eef3c3b907d6f396ebfdc693b12
+ environments/settings/teams/organization_role: 979b75fcc3696952e5922d659c839c10
environments/settings/teams/owner_role_description: 8f577e6f9d1368fed4eba5a91ffc8cbf
environments/settings/teams/please_fill_all_member_fields: 60e38d9906ec9a02a44d16c736bd9fe9
environments/settings/teams/please_fill_all_project_fields: 6712059df63c432ecd31f3c52b8e4d87
environments/settings/teams/read: 2494ca23d10e5b6381eb271aceeb5270
environments/settings/teams/read_write: 278a90dade128198d4c93ac00c345320
+ environments/settings/teams/select_member: 7f4a38312aabbbe3fe92756b57bd5d75
+ environments/settings/teams/select_project: 6e4f4a24178660851d9ae0874706be9f
environments/settings/teams/team_admin: 5df68214685738029af678ae1d5912bb
environments/settings/teams/team_created_successfully: 45f83048fcabf466551144858a761eca
environments/settings/teams/team_deleted_successfully: 972c86b0abe87f229f7bf1a691c0a253
diff --git a/apps/web/locales/de-DE.json b/apps/web/locales/de-DE.json
index f6f5f6f022..53a271ecfd 100644
--- a/apps/web/locales/de-DE.json
+++ b/apps/web/locales/de-DE.json
@@ -261,6 +261,7 @@
"maximum": "Maximal",
"member": "Mitglied",
"members": "Mitglieder",
+ "members_and_teams": "Mitglieder & Teams",
"membership_not_found": "Mitgliedschaft nicht gefunden",
"metadata": "Metadaten",
"minimum": "Minimum",
@@ -340,6 +341,7 @@
"read_docs": "Dokumentation lesen",
"recipients": "Empfänger",
"remove": "Entfernen",
+ "remove_from_team": "Aus Team entfernen",
"reorder_and_hide_columns": "Spalten neu anordnen und ausblenden",
"report_survey": "Umfrage melden",
"request_pricing": "Preise anfragen",
@@ -407,7 +409,8 @@
"team_access": "Teamzugriff",
"team_id": "Team-ID",
"team_name": "Teamname",
- "teams": "Zugriffskontrolle",
+ "team_role": "Team-Rolle",
+ "teams": "Teams",
"teams_not_found": "Teams nicht gefunden",
"text": "Text",
"time": "Zeit",
@@ -903,7 +906,6 @@
"teams": {
"manage_teams": "Teams verwalten",
"no_teams_found": "Keine Teams gefunden",
- "only_organization_owners_and_managers_can_manage_teams": "Nur Organisationsinhaber und -manager können Teams verwalten.",
"permission": "Berechtigung",
"team_name": "Teamname",
"team_settings_description": "Teams und ihre Mitglieder können auf dieses Projekt und seine Umfragen zugreifen. Organisationsbesitzer und Manager können diesen Zugriff gewähren."
@@ -1167,13 +1169,17 @@
"manage_team": "Team verwalten",
"manage_team_disabled": "Nur Organisationsbesitzer, Manager und Team-Admins können Teams verwalten.",
"manager_role_description": "Manager können auf alle Projekte zugreifen und Mitglieder hinzufügen und entfernen.",
+ "member": "Mitglied",
"member_role_description": "Mitglieder können in ausgewählten Projekten arbeiten.",
"member_role_info_message": "Um neuen Mitgliedern Zugriff auf ein Projekt zu geben, füge sie bitte unten einem Team hinzu. Mit Teams kannst du steuern, wer auf welches Projekt zugreifen kann.",
+ "organization_role": "Organisationsrolle",
"owner_role_description": "Besitzer haben die volle Kontrolle über die Organisation.",
"please_fill_all_member_fields": "Bitte fülle alle Felder aus, um ein neues Mitglied hinzuzufügen.",
"please_fill_all_project_fields": "Bitte fülle alle Felder aus, um ein neues Projekt hinzuzufügen.",
"read": "Lesen",
"read_write": "Lesen & Schreiben",
+ "select_member": "Mitglied auswählen",
+ "select_project": "Projekt auswählen",
"team_admin": "Team-Admin",
"team_created_successfully": "Team erfolgreich erstellt.",
"team_deleted_successfully": "Team erfolgreich gelöscht.",
diff --git a/apps/web/locales/en-US.json b/apps/web/locales/en-US.json
index ee5fa83ee8..5893d65434 100644
--- a/apps/web/locales/en-US.json
+++ b/apps/web/locales/en-US.json
@@ -261,6 +261,7 @@
"maximum": "Maximum",
"member": "Member",
"members": "Members",
+ "members_and_teams": "Members & Teams",
"membership_not_found": "Membership not found",
"metadata": "Metadata",
"minimum": "Minimum",
@@ -337,9 +338,10 @@
"quota": "Quota",
"quotas": "Quotas",
"quotas_description": "Limit the amount of responses you receive from participants who meet certain criteria.",
- "read_docs": "Read Docs",
+ "read_docs": "Read docs",
"recipients": "Recipients",
"remove": "Remove",
+ "remove_from_team": "Remove from team",
"reorder_and_hide_columns": "Reorder and hide columns",
"report_survey": "Report Survey",
"request_pricing": "Request Pricing",
@@ -349,7 +351,6 @@
"responses": "Responses",
"restart": "Restart",
"role": "Role",
- "role_organization": "Role (Organization)",
"saas": "SaaS",
"sales": "Sales",
"save": "Save",
@@ -407,7 +408,8 @@
"team_access": "Team Access",
"team_id": "Team ID",
"team_name": "Team name",
- "teams": "Access Control",
+ "team_role": "Team role",
+ "teams": "Teams",
"teams_not_found": "Teams not found",
"text": "Text",
"time": "Time",
@@ -903,7 +905,6 @@
"teams": {
"manage_teams": "Manage teams",
"no_teams_found": "No teams found",
- "only_organization_owners_and_managers_can_manage_teams": "Only organization owners and managers can manage teams.",
"permission": "Permission",
"team_name": "Team Name",
"team_settings_description": "See which teams can access this project."
@@ -1167,13 +1168,17 @@
"manage_team": "Manage team",
"manage_team_disabled": "Only organization owners, managers and team admins can manage teams.",
"manager_role_description": "Managers can access all projects and add and remove members.",
+ "member": "Member",
"member_role_description": "Members can work in selected projects.",
"member_role_info_message": "To give new members access to a project, please add them to a Team below. With Teams you can manage who has access to which project.",
+ "organization_role": "Organization role",
"owner_role_description": "Owners have full control over the organization.",
"please_fill_all_member_fields": "Please fill all the fields to add a new member.",
"please_fill_all_project_fields": "Please fill all the fields to add a new project.",
"read": "Read",
"read_write": "Read & Write",
+ "select_member": "Select member",
+ "select_project": "Select project",
"team_admin": "Team Admin",
"team_created_successfully": "Team created successfully.",
"team_deleted_successfully": "Team deleted successfully.",
diff --git a/apps/web/locales/es-ES.json b/apps/web/locales/es-ES.json
index 42544916e9..2b49aa2a5a 100644
--- a/apps/web/locales/es-ES.json
+++ b/apps/web/locales/es-ES.json
@@ -261,6 +261,7 @@
"maximum": "Máximo",
"member": "Miembro",
"members": "Miembros",
+ "members_and_teams": "Miembros y equipos",
"membership_not_found": "Membresía no encontrada",
"metadata": "Metadatos",
"minimum": "Mínimo",
@@ -340,6 +341,7 @@
"read_docs": "Leer documentación",
"recipients": "Destinatarios",
"remove": "Eliminar",
+ "remove_from_team": "Eliminar del equipo",
"reorder_and_hide_columns": "Reordenar y ocultar columnas",
"report_survey": "Reportar encuesta",
"request_pricing": "Solicitar precios",
@@ -407,7 +409,8 @@
"team_access": "Acceso de equipo",
"team_id": "ID de equipo",
"team_name": "Nombre del equipo",
- "teams": "Control de acceso",
+ "team_role": "Rol del equipo",
+ "teams": "Equipos",
"teams_not_found": "Equipos no encontrados",
"text": "Texto",
"time": "Hora",
@@ -903,7 +906,6 @@
"teams": {
"manage_teams": "Gestionar equipos",
"no_teams_found": "No se han encontrado equipos",
- "only_organization_owners_and_managers_can_manage_teams": "Solo los propietarios y gestores de la organización pueden gestionar equipos.",
"permission": "Permiso",
"team_name": "Nombre del equipo",
"team_settings_description": "Consulta qué equipos pueden acceder a este proyecto."
@@ -1167,13 +1169,17 @@
"manage_team": "Gestionar equipo",
"manage_team_disabled": "Solo los propietarios de la organización, gestores y administradores de equipo pueden gestionar equipos.",
"manager_role_description": "Los gestores pueden acceder a todos los proyectos y añadir y eliminar miembros.",
+ "member": "Miembro",
"member_role_description": "Los miembros pueden trabajar en proyectos seleccionados.",
"member_role_info_message": "Para dar a los nuevos miembros acceso a un proyecto, por favor añádelos a un equipo a continuación. Con los equipos puedes gestionar quién tiene acceso a qué proyecto.",
+ "organization_role": "Rol en la organización",
"owner_role_description": "Los propietarios tienen control total sobre la organización.",
"please_fill_all_member_fields": "Por favor, rellena todos los campos para añadir un nuevo miembro.",
"please_fill_all_project_fields": "Por favor, rellena todos los campos para añadir un nuevo proyecto.",
"read": "Lectura",
"read_write": "Lectura y escritura",
+ "select_member": "Seleccionar miembro",
+ "select_project": "Seleccionar proyecto",
"team_admin": "Administrador de equipo",
"team_created_successfully": "Equipo creado con éxito.",
"team_deleted_successfully": "Equipo eliminado correctamente.",
diff --git a/apps/web/locales/fr-FR.json b/apps/web/locales/fr-FR.json
index 46cd55662e..8fef157bb1 100644
--- a/apps/web/locales/fr-FR.json
+++ b/apps/web/locales/fr-FR.json
@@ -261,6 +261,7 @@
"maximum": "Max",
"member": "Membre",
"members": "Membres",
+ "members_and_teams": "Membres & Équipes",
"membership_not_found": "Abonnement non trouvé",
"metadata": "Métadonnées",
"minimum": "Min",
@@ -340,6 +341,7 @@
"read_docs": "Lire les documents",
"recipients": "Destinataires",
"remove": "Retirer",
+ "remove_from_team": "Retirer de l'équipe",
"reorder_and_hide_columns": "Réorganiser et masquer des colonnes",
"report_survey": "Rapport d'enquête",
"request_pricing": "Connaître le tarif",
@@ -407,7 +409,8 @@
"team_access": "Accès",
"team_id": "Identifiant de l'équipe",
"team_name": "Nom de l'équipe",
- "teams": "Contrôle d'accès",
+ "team_role": "Rôle dans l'équipe",
+ "teams": "Équipes",
"teams_not_found": "Équipes non trouvées",
"text": "Texte",
"time": "Temps",
@@ -903,7 +906,6 @@
"teams": {
"manage_teams": "Gérer les équipes",
"no_teams_found": "Aucune équipe trouvée",
- "only_organization_owners_and_managers_can_manage_teams": "Seuls les propriétaires et les gestionnaires de l'organisation peuvent gérer les équipes.",
"permission": "Permission",
"team_name": "Nom de l'équipe",
"team_settings_description": "Vous pouvez consulter la liste des équipes qui ont accès à ce projet."
@@ -1167,13 +1169,17 @@
"manage_team": "Gérer l'équipe",
"manage_team_disabled": "Seuls les propriétaires de l'organisation, les gestionnaires et les administrateurs d'équipe peuvent gérer les équipes.",
"manager_role_description": "Les gestionnaires peuvent accéder à tous les projets et ajouter et supprimer des membres.",
+ "member": "Membre",
"member_role_description": "Les membres peuvent travailler sur des projets sélectionnés.",
"member_role_info_message": "Pour donner accès à un projet aux nouveaux membres, veuillez les ajouter à une équipe ci-dessous. Avec les équipes, vous pouvez gérer qui a accès à quel projet.",
+ "organization_role": "Rôle dans l'organisation",
"owner_role_description": "Les propriétaires ont un contrôle total sur l'organisation.",
"please_fill_all_member_fields": "Veuillez remplir tous les champs pour ajouter un nouveau membre.",
"please_fill_all_project_fields": "Veuillez remplir tous les champs pour ajouter un nouveau projet.",
"read": "Lire",
"read_write": "Lire et Écrire",
+ "select_member": "Sélectionner membre",
+ "select_project": "Sélectionner projet",
"team_admin": "Administrateur d'équipe",
"team_created_successfully": "Équipe créée avec succès.",
"team_deleted_successfully": "Équipe supprimée avec succès.",
diff --git a/apps/web/locales/ja-JP.json b/apps/web/locales/ja-JP.json
index ee26fb6f0d..3be1eae10d 100644
--- a/apps/web/locales/ja-JP.json
+++ b/apps/web/locales/ja-JP.json
@@ -261,6 +261,7 @@
"maximum": "最大",
"member": "メンバー",
"members": "メンバー",
+ "members_and_teams": "メンバー&チーム",
"membership_not_found": "メンバーシップが見つかりません",
"metadata": "メタデータ",
"minimum": "最小",
@@ -340,6 +341,7 @@
"read_docs": "ドキュメントを読む",
"recipients": "受信者",
"remove": "削除",
+ "remove_from_team": "チームから削除",
"reorder_and_hide_columns": "列の並び替えと非表示",
"report_survey": "フォームを報告",
"request_pricing": "料金を問い合わせる",
@@ -407,7 +409,8 @@
"team_access": "チームアクセス",
"team_id": "チームID",
"team_name": "チーム名",
- "teams": "アクセス制御",
+ "team_role": "チームの役割",
+ "teams": "チーム",
"teams_not_found": "チームが見つかりません",
"text": "テキスト",
"time": "時間",
@@ -903,7 +906,6 @@
"teams": {
"manage_teams": "チームを管理",
"no_teams_found": "チームが見つかりません",
- "only_organization_owners_and_managers_can_manage_teams": "組織のオーナーまたは管理者のみがチームを管理できます。",
"permission": "権限",
"team_name": "チーム名",
"team_settings_description": "このプロジェクトにアクセスできるチームを確認します。"
@@ -1167,13 +1169,17 @@
"manage_team": "チームを管理",
"manage_team_disabled": "組織のオーナー、管理者、チーム管理者のみがチームを管理できます。",
"manager_role_description": "管理者はすべてのプロジェクトにアクセスでき、メンバーを追加および削除できます。",
+ "member": "メンバー",
"member_role_description": "メンバーは選択されたプロジェクトで作業できます。",
"member_role_info_message": "新しいメンバーにプロジェクトへのアクセス権を付与するには、以下のチームに追加してください。チームを使用すると、誰がどのプロジェクトにアクセスできるかを管理できます。",
+ "organization_role": "組織の役割",
"owner_role_description": "オーナーは組織を完全に制御できます。",
"please_fill_all_member_fields": "新しいメンバーを追加するには、すべてのフィールドを記入してください。",
"please_fill_all_project_fields": "新しいプロジェクトを追加するには、すべてのフィールドを記入してください。",
"read": "読み取り",
"read_write": "読み書き",
+ "select_member": "メンバーを選択",
+ "select_project": "プロジェクトを選択",
"team_admin": "チーム管理者",
"team_created_successfully": "チームを正常に作成しました。",
"team_deleted_successfully": "チームを正常に削除しました。",
diff --git a/apps/web/locales/nl-NL.json b/apps/web/locales/nl-NL.json
index 5f9d7b36e8..eeb63ca0f5 100644
--- a/apps/web/locales/nl-NL.json
+++ b/apps/web/locales/nl-NL.json
@@ -261,6 +261,7 @@
"maximum": "Maximaal",
"member": "Lid",
"members": "Leden",
+ "members_and_teams": "Leden & teams",
"membership_not_found": "Lidmaatschap niet gevonden",
"metadata": "Metagegevens",
"minimum": "Minimum",
@@ -340,6 +341,7 @@
"read_docs": "Lees Documenten",
"recipients": "Ontvangers",
"remove": "Verwijderen",
+ "remove_from_team": "Verwijderen uit team",
"reorder_and_hide_columns": "Kolommen opnieuw rangschikken en verbergen",
"report_survey": "Verslag enquête",
"request_pricing": "Vraag prijzen aan",
@@ -407,7 +409,8 @@
"team_access": "Teamtoegang",
"team_id": "Team-ID",
"team_name": "Teamnaam",
- "teams": "Toegangscontrole",
+ "team_role": "Teamrol",
+ "teams": "Teams",
"teams_not_found": "Teams niet gevonden",
"text": "Tekst",
"time": "Tijd",
@@ -903,7 +906,6 @@
"teams": {
"manage_teams": "Beheer teams",
"no_teams_found": "Geen teams gevonden",
- "only_organization_owners_and_managers_can_manage_teams": "Alleen eigenaren en managers van organisaties kunnen teams beheren.",
"permission": "Toestemming",
"team_name": "Teamnaam",
"team_settings_description": "Bekijk welke teams toegang hebben tot dit project."
@@ -1167,13 +1169,17 @@
"manage_team": "Beheer team",
"manage_team_disabled": "Alleen organisatie-eigenaren, managers en teambeheerders kunnen teams beheren.",
"manager_role_description": "Managers hebben toegang tot alle projecten en kunnen leden toevoegen en verwijderen.",
+ "member": "Lid",
"member_role_description": "Leden kunnen in geselecteerde projecten werken.",
"member_role_info_message": "Om nieuwe leden toegang te geven tot een project, voegt u ze hieronder toe aan een team. Met Teams kun je beheren wie toegang heeft tot welk project.",
+ "organization_role": "Organisatierol",
"owner_role_description": "Eigenaars hebben volledige controle over de organisatie.",
"please_fill_all_member_fields": "Vul alle velden in om een nieuw lid toe te voegen.",
"please_fill_all_project_fields": "Vul alle velden in om een nieuw project toe te voegen.",
"read": "Lezen",
"read_write": "Lezen en schrijven",
+ "select_member": "Selecteer lid",
+ "select_project": "Selecteer project",
"team_admin": "Teambeheerder",
"team_created_successfully": "Team succesvol aangemaakt.",
"team_deleted_successfully": "Team succesvol verwijderd.",
diff --git a/apps/web/locales/pt-BR.json b/apps/web/locales/pt-BR.json
index 89da514535..effc6967ed 100644
--- a/apps/web/locales/pt-BR.json
+++ b/apps/web/locales/pt-BR.json
@@ -261,6 +261,7 @@
"maximum": "Máximo",
"member": "Membros",
"members": "Membros",
+ "members_and_teams": "Membros e equipes",
"membership_not_found": "Assinatura não encontrada",
"metadata": "metadados",
"minimum": "Mínimo",
@@ -340,6 +341,7 @@
"read_docs": "Ler Documentação",
"recipients": "Destinatários",
"remove": "remover",
+ "remove_from_team": "Remover da equipe",
"reorder_and_hide_columns": "Reordenar e ocultar colunas",
"report_survey": "Relatório de Pesquisa",
"request_pricing": "Solicitar Preços",
@@ -407,7 +409,8 @@
"team_access": "Acesso da equipe",
"team_id": "ID da Equipe",
"team_name": "Nome da equipe",
- "teams": "Controle de Acesso",
+ "team_role": "Função na equipe",
+ "teams": "Equipes",
"teams_not_found": "Equipes não encontradas",
"text": "Texto",
"time": "tempo",
@@ -903,7 +906,6 @@
"teams": {
"manage_teams": "Gerenciar Equipes",
"no_teams_found": "Nenhuma equipe encontrada",
- "only_organization_owners_and_managers_can_manage_teams": "Apenas proprietários e gerentes da organização podem gerenciar equipes.",
"permission": "Permissão",
"team_name": "Nome da equipe",
"team_settings_description": "As equipes e seus membros podem acessar este projeto e suas pesquisas. Proprietários e gerentes da organização podem conceder esse acesso."
@@ -1167,13 +1169,17 @@
"manage_team": "Gerenciar equipe",
"manage_team_disabled": "Apenas proprietários da organização, gerentes e administradores da equipe podem gerenciar equipes.",
"manager_role_description": "Os gerentes podem acessar todos os projetos e adicionar e remover membros.",
+ "member": "Membro",
"member_role_description": "Os membros podem trabalhar em projetos selecionados.",
"member_role_info_message": "Para dar acesso a novos membros a um projeto, por favor, adicione-os a uma equipe abaixo. Com equipes, você pode gerenciar quem tem acesso a qual projeto.",
+ "organization_role": "Função na organização",
"owner_role_description": "Os proprietários têm controle total sobre a organização.",
"please_fill_all_member_fields": "Por favor, preencha todos os campos para adicionar um novo membro.",
"please_fill_all_project_fields": "Por favor, preencha todos os campos para adicionar um novo projeto.",
"read": "Leitura",
"read_write": "Leitura & Escrita",
+ "select_member": "Selecionar membro",
+ "select_project": "Selecionar projeto",
"team_admin": "Administrador da equipe",
"team_created_successfully": "Equipe criada com sucesso.",
"team_deleted_successfully": "Equipe excluída com sucesso.",
diff --git a/apps/web/locales/pt-PT.json b/apps/web/locales/pt-PT.json
index 466315bc32..36648ae707 100644
--- a/apps/web/locales/pt-PT.json
+++ b/apps/web/locales/pt-PT.json
@@ -261,6 +261,7 @@
"maximum": "Máximo",
"member": "Membro",
"members": "Membros",
+ "members_and_teams": "Membros e equipas",
"membership_not_found": "Associação não encontrada",
"metadata": "Metadados",
"minimum": "Mínimo",
@@ -340,6 +341,7 @@
"read_docs": "Ler Documentos",
"recipients": "Destinatários",
"remove": "Remover",
+ "remove_from_team": "Remover da equipa",
"reorder_and_hide_columns": "Reordenar e ocultar colunas",
"report_survey": "Relatório de Inquérito",
"request_pricing": "Pedido de Preços",
@@ -407,7 +409,8 @@
"team_access": "Acesso da Equipa",
"team_id": "ID da Equipa",
"team_name": "Nome da equipa",
- "teams": "Controlo de Acesso",
+ "team_role": "Função na equipa",
+ "teams": "Equipas",
"teams_not_found": "Equipas não encontradas",
"text": "Texto",
"time": "Tempo",
@@ -903,7 +906,6 @@
"teams": {
"manage_teams": "Gerir equipas",
"no_teams_found": "Nenhuma equipa encontrada",
- "only_organization_owners_and_managers_can_manage_teams": "Apenas os proprietários e gestores da organização podem gerir equipas.",
"permission": "Permissão",
"team_name": "Nome da Equipa",
"team_settings_description": "Veja quais equipas podem aceder a este projeto."
@@ -1167,13 +1169,17 @@
"manage_team": "Gerir equipa",
"manage_team_disabled": "Apenas os proprietários da organização, gestores e administradores de equipa podem gerir equipas.",
"manager_role_description": "Os gestores podem aceder a todos os projetos e adicionar e remover membros.",
+ "member": "Membro",
"member_role_description": "Os membros podem trabalhar em projetos selecionados.",
"member_role_info_message": "Adicione os membros que deseja a uma Equipa abaixo. Nesta secção, pode gerir quem tem acesso a cada projeto.",
+ "organization_role": "Função na organização",
"owner_role_description": "Os proprietários têm controlo total sobre a organização.",
"please_fill_all_member_fields": "Por favor, preencha todos os campos para adicionar um novo membro.",
"please_fill_all_project_fields": "Por favor, preencha todos os campos para adicionar um novo projeto.",
"read": "Ler",
"read_write": "Ler e Escrever",
+ "select_member": "Selecionar membro",
+ "select_project": "Selecionar projeto",
"team_admin": "Administrador da Equipa",
"team_created_successfully": "Equipa criada com sucesso.",
"team_deleted_successfully": "Equipa eliminada com sucesso.",
diff --git a/apps/web/locales/ro-RO.json b/apps/web/locales/ro-RO.json
index 7faa1e952b..a2e611096e 100644
--- a/apps/web/locales/ro-RO.json
+++ b/apps/web/locales/ro-RO.json
@@ -261,6 +261,7 @@
"maximum": "Maximum",
"member": "Membru",
"members": "Membri",
+ "members_and_teams": "Membri și echipe",
"membership_not_found": "Apartenența nu a fost găsită",
"metadata": "Metadate",
"minimum": "Minim",
@@ -340,6 +341,7 @@
"read_docs": "Citește documentația",
"recipients": "Destinatari",
"remove": "Șterge",
+ "remove_from_team": "Elimină din echipă",
"reorder_and_hide_columns": "Reordonați și ascundeți coloanele",
"report_survey": "Raportează chestionarul",
"request_pricing": "Solicită Prețuri",
@@ -407,7 +409,8 @@
"team_access": "Acces echipă",
"team_id": "ID echipă",
"team_name": "Nume echipă",
- "teams": "Control acces",
+ "team_role": "Rol în echipă",
+ "teams": "Echipe",
"teams_not_found": "Echipele nu au fost găsite",
"text": "Text",
"time": "Timp",
@@ -903,7 +906,6 @@
"teams": {
"manage_teams": "Gestionați echipele",
"no_teams_found": "Nicio echipă găsită",
- "only_organization_owners_and_managers_can_manage_teams": "Doar proprietarii de organizație și managerii pot gestiona echipele.",
"permission": "Permisiune",
"team_name": "Nume echipă",
"team_settings_description": "Vezi care echipe pot accesa acest proiect."
@@ -1167,13 +1169,17 @@
"manage_team": "Gestionați echipa",
"manage_team_disabled": "Doar proprietarii de organizații, managerii și administratorii de echipă pot gestiona echipele.",
"manager_role_description": "Managerii pot accesa toate proiectele și pot adăuga sau elimina membri.",
+ "member": "Membru",
"member_role_description": "Membrii pot lucra în proiectele selectate.",
"member_role_info_message": "Pentru a oferi membrilor noi acces la un proiect, vă rugăm să-i adăugați la o Echipă mai jos. Cu Echipe puteți gestiona cine are acces la ce proiect.",
+ "organization_role": "Rol în organizație",
"owner_role_description": "Proprietarii au control total asupra organizației.",
"please_fill_all_member_fields": "Vă rugăm să completați toate câmpurile pentru a adăuga un nou membru.",
"please_fill_all_project_fields": "Vă rugăm să completați toate câmpurile pentru a adăuga un proiect nou.",
"read": "Citește",
"read_write": "Citire & Scriere",
+ "select_member": "Selectează membrul",
+ "select_project": "Selectează proiectul",
"team_admin": "Administrator Echipe",
"team_created_successfully": "Echipă creată cu succes",
"team_deleted_successfully": "Echipă ștearsă cu succes.",
diff --git a/apps/web/locales/sv-SE.json b/apps/web/locales/sv-SE.json
index d46be0c3fb..268221f15f 100644
--- a/apps/web/locales/sv-SE.json
+++ b/apps/web/locales/sv-SE.json
@@ -261,6 +261,7 @@
"maximum": "Maximum",
"member": "Medlem",
"members": "Medlemmar",
+ "members_and_teams": "Medlemmar och team",
"membership_not_found": "Medlemskap hittades inte",
"metadata": "Metadata",
"minimum": "Minimum",
@@ -340,6 +341,7 @@
"read_docs": "Läs dokumentation",
"recipients": "Mottagare",
"remove": "Ta bort",
+ "remove_from_team": "Ta bort från teamet",
"reorder_and_hide_columns": "Ordna om och dölj kolumner",
"report_survey": "Rapportera enkät",
"request_pricing": "Begär prissättning",
@@ -407,6 +409,7 @@
"team_access": "Teamåtkomst",
"team_id": "Team-ID",
"team_name": "Teamnamn",
+ "team_role": "Teamroll",
"teams": "Åtkomstkontroll",
"teams_not_found": "Team hittades inte",
"text": "Text",
@@ -903,7 +906,6 @@
"teams": {
"manage_teams": "Hantera team",
"no_teams_found": "Inga team hittades",
- "only_organization_owners_and_managers_can_manage_teams": "Endast organisationsägare och administratörer kan hantera team.",
"permission": "Behörighet",
"team_name": "Teamnamn",
"team_settings_description": "Se vilka team som kan komma åt detta projekt."
@@ -1167,13 +1169,17 @@
"manage_team": "Hantera team",
"manage_team_disabled": "Endast organisationsägare, administratörer och teamadministratörer kan hantera team.",
"manager_role_description": "Administratörer kan komma åt alla projekt och lägga till och ta bort medlemmar.",
+ "member": "Medlem",
"member_role_description": "Medlemmar kan arbeta i valda projekt.",
"member_role_info_message": "För att ge nya medlemmar åtkomst till ett projekt, vänligen lägg till dem i ett team nedan. Med team kan du hantera vem som har åtkomst till vilket projekt.",
+ "organization_role": "Organisationsroll",
"owner_role_description": "Ägare har full kontroll över organisationen.",
"please_fill_all_member_fields": "Vänligen fyll i alla fält för att lägga till en ny medlem.",
"please_fill_all_project_fields": "Vänligen fyll i alla fält för att lägga till ett nytt projekt.",
"read": "Läs",
"read_write": "Läs och skriv",
+ "select_member": "Välj medlem",
+ "select_project": "Välj projekt",
"team_admin": "Teamadministratör",
"team_created_successfully": "Team skapat.",
"team_deleted_successfully": "Team borttaget.",
diff --git a/apps/web/locales/zh-Hans-CN.json b/apps/web/locales/zh-Hans-CN.json
index f8ea6ed123..eaa8632fcf 100644
--- a/apps/web/locales/zh-Hans-CN.json
+++ b/apps/web/locales/zh-Hans-CN.json
@@ -261,6 +261,7 @@
"maximum": "最大值",
"member": "成员",
"members": "成员",
+ "members_and_teams": "成员和团队",
"membership_not_found": "未找到会员资格",
"metadata": "元数据",
"minimum": "最低",
@@ -340,6 +341,7 @@
"read_docs": "阅读 文档",
"recipients": "收件人",
"remove": "移除",
+ "remove_from_team": "从团队中移除",
"reorder_and_hide_columns": "重新排序和隐藏列",
"report_survey": "报告调查",
"request_pricing": "请求 定价",
@@ -407,7 +409,8 @@
"team_access": "团队 访问",
"team_id": "团队 ID",
"team_name": "团队 名称",
- "teams": "访问控制",
+ "team_role": "团队角色",
+ "teams": "团队",
"teams_not_found": "未找到 团队",
"text": "文本",
"time": "时间",
@@ -903,7 +906,6 @@
"teams": {
"manage_teams": "管理 团队",
"no_teams_found": "未找到 团队",
- "only_organization_owners_and_managers_can_manage_teams": "只有 组织 拥有者 和 经理 可以 管理 团队。",
"permission": "权限",
"team_name": "团队名称",
"team_settings_description": "查看 哪些 团队 可以 访问 该 项目。"
@@ -1167,13 +1169,17 @@
"manage_team": "管理团队",
"manage_team_disabled": "只有 组织 拥有者、经理 和 团队 管理员 可以 管理 团队。",
"manager_role_description": "经理 可以 访问 所有 项目 并 添加 移除 成员。",
+ "member": "成员",
"member_role_description": "成员 可以 在 选定 项目 中 工作。",
"member_role_info_message": "要 给 新 成员 访问 项目 ,请 将 他们 添加 到 下方 的 团队 。通过 团队 你 可以 管理 谁 可以 访问 哪个 项目 。",
+ "organization_role": "组织角色",
"owner_role_description": "所有者拥有对组织的完全控制权。",
"please_fill_all_member_fields": "请 填写 所有 字段 以 添加 新 成员。",
"please_fill_all_project_fields": "请 填写 所有 字段 以 添加 新 项目。",
"read": "阅读",
"read_write": "读 & 写",
+ "select_member": "选择成员",
+ "select_project": "选择项目",
"team_admin": "团队管理员",
"team_created_successfully": "团队 创建 成功",
"team_deleted_successfully": "团队 删除 成功",
diff --git a/apps/web/locales/zh-Hant-TW.json b/apps/web/locales/zh-Hant-TW.json
index 19fbd6e996..ebdbbf045c 100644
--- a/apps/web/locales/zh-Hant-TW.json
+++ b/apps/web/locales/zh-Hant-TW.json
@@ -261,6 +261,7 @@
"maximum": "最大值",
"member": "成員",
"members": "成員",
+ "members_and_teams": "成員與團隊",
"membership_not_found": "找不到成員資格",
"metadata": "元數據",
"minimum": "最小值",
@@ -340,6 +341,7 @@
"read_docs": "閱讀文件",
"recipients": "收件者",
"remove": "移除",
+ "remove_from_team": "從團隊中移除",
"reorder_and_hide_columns": "重新排序和隱藏欄位",
"report_survey": "報告問卷",
"request_pricing": "請求定價",
@@ -407,7 +409,8 @@
"team_access": "團隊存取權限",
"team_id": "團隊 ID",
"team_name": "團隊名稱",
- "teams": "存取控制",
+ "team_role": "團隊角色",
+ "teams": "團隊",
"teams_not_found": "找不到團隊",
"text": "文字",
"time": "時間",
@@ -903,7 +906,6 @@
"teams": {
"manage_teams": "管理團隊",
"no_teams_found": "找不到團隊",
- "only_organization_owners_and_managers_can_manage_teams": "只有組織擁有者和管理員才能管理團隊。",
"permission": "權限",
"team_name": "團隊名稱",
"team_settings_description": "查看哪些團隊可以存取此專案。"
@@ -1167,13 +1169,17 @@
"manage_team": "管理團隊",
"manage_team_disabled": "只有組織擁有者、管理員和團隊管理員才能管理團隊。",
"manager_role_description": "管理員可以存取所有專案,並新增和移除成員。",
+ "member": "成員",
"member_role_description": "成員可以在選定的專案中工作。",
"member_role_info_message": "若要授予新成員存取專案的權限,請將他們新增至下方的團隊。藉由團隊,您可以管理誰可以存取哪些專案。",
+ "organization_role": "組織角色",
"owner_role_description": "擁有者對組織具有完全控制權。",
"please_fill_all_member_fields": "請填寫所有欄位以新增新成員。",
"please_fill_all_project_fields": "請填寫所有欄位以新增新專案。",
"read": "讀取",
"read_write": "讀取和寫入",
+ "select_member": "選擇成員",
+ "select_project": "選擇專案",
"team_admin": "團隊管理員",
"team_created_successfully": "團隊已成功建立。",
"team_deleted_successfully": "團隊已成功刪除。",
diff --git a/apps/web/modules/auth/signup/lib/__tests__/team.test.ts b/apps/web/modules/auth/signup/lib/__tests__/team.test.ts
index 026c1e4094..d8821e31fe 100644
--- a/apps/web/modules/auth/signup/lib/__tests__/team.test.ts
+++ b/apps/web/modules/auth/signup/lib/__tests__/team.test.ts
@@ -3,7 +3,7 @@ import { OrganizationRole } from "@prisma/client";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { CreateMembershipInvite } from "@/modules/auth/signup/types/invites";
-import { createTeamMembership } from "../team";
+import { createTeamMembership, getTeamProjectIds } from "../team";
// Setup all mocks
const setupMocks = () => {
@@ -31,6 +31,7 @@ const setupMocks = () => {
vi.mock("@formbricks/logger", () => ({
logger: {
error: vi.fn(),
+ warn: vi.fn(),
},
}));
@@ -55,7 +56,7 @@ describe("Team Management", () => {
describe("createTeamMembership", () => {
describe("when user is an admin", () => {
test("creates a team membership with admin role", async () => {
- vi.mocked(prisma.team.findUnique).mockResolvedValue(MOCK_TEAM);
+ vi.mocked(prisma.team.findUnique).mockResolvedValue(MOCK_TEAM as unknown as any);
vi.mocked(prisma.teamUser.create).mockResolvedValue(MOCK_TEAM_USER);
await createTeamMembership(MOCK_INVITE, MOCK_IDS.userId);
@@ -90,7 +91,7 @@ describe("Team Management", () => {
role: "member" as OrganizationRole,
};
- vi.mocked(prisma.team.findUnique).mockResolvedValue(MOCK_TEAM);
+ vi.mocked(prisma.team.findUnique).mockResolvedValue(MOCK_TEAM as unknown as any);
vi.mocked(prisma.teamUser.create).mockResolvedValue({
...MOCK_TEAM_USER,
role: "contributor",
@@ -110,11 +111,68 @@ describe("Team Management", () => {
describe("error handling", () => {
test("throws error when database operation fails", async () => {
- vi.mocked(prisma.team.findUnique).mockResolvedValue(MOCK_TEAM);
+ vi.mocked(prisma.team.findUnique).mockResolvedValue(MOCK_TEAM as unknown as any);
vi.mocked(prisma.teamUser.create).mockRejectedValue(new Error("Database error"));
await expect(createTeamMembership(MOCK_INVITE, MOCK_IDS.userId)).rejects.toThrow("Database error");
});
});
+
+ describe("when team does not exist", () => {
+ test("skips membership creation and continues to next team", async () => {
+ const inviteWithMultipleTeams: CreateMembershipInvite = {
+ ...MOCK_INVITE,
+ teamIds: ["non-existent-team", MOCK_IDS.teamId],
+ };
+
+ vi.mocked(prisma.team.findUnique)
+ .mockResolvedValueOnce(null)
+ .mockResolvedValueOnce(MOCK_TEAM as unknown as any);
+ vi.mocked(prisma.teamUser.create).mockResolvedValue(MOCK_TEAM_USER);
+
+ await createTeamMembership(inviteWithMultipleTeams, MOCK_IDS.userId);
+
+ expect(prisma.team.findUnique).toHaveBeenCalledTimes(2);
+ expect(prisma.teamUser.create).toHaveBeenCalledTimes(1);
+ expect(prisma.teamUser.create).toHaveBeenCalledWith({
+ data: {
+ teamId: MOCK_IDS.teamId,
+ userId: MOCK_IDS.userId,
+ role: "admin",
+ },
+ });
+ });
+ });
+ });
+
+ describe("getTeamProjectIds", () => {
+ test("returns team with projectTeams when team exists", async () => {
+ vi.mocked(prisma.team.findUnique).mockResolvedValue(MOCK_TEAM as unknown as any);
+
+ const result = await getTeamProjectIds(MOCK_IDS.teamId, MOCK_IDS.organizationId);
+
+ expect(result).toEqual(MOCK_TEAM);
+ expect(prisma.team.findUnique).toHaveBeenCalledWith({
+ where: {
+ id: MOCK_IDS.teamId,
+ organizationId: MOCK_IDS.organizationId,
+ },
+ select: {
+ projectTeams: {
+ select: {
+ projectId: true,
+ },
+ },
+ },
+ });
+ });
+
+ test("returns null when team does not exist", async () => {
+ vi.mocked(prisma.team.findUnique).mockResolvedValue(null);
+
+ const result = await getTeamProjectIds(MOCK_IDS.teamId, MOCK_IDS.organizationId);
+
+ expect(result).toBeNull();
+ });
});
});
diff --git a/apps/web/modules/auth/signup/lib/team.ts b/apps/web/modules/auth/signup/lib/team.ts
index 1fd81fe294..7f1324a9a4 100644
--- a/apps/web/modules/auth/signup/lib/team.ts
+++ b/apps/web/modules/auth/signup/lib/team.ts
@@ -18,15 +18,18 @@ export const createTeamMembership = async (invite: CreateMembershipInvite, userI
for (const teamId of teamIds) {
const team = await getTeamProjectIds(teamId, invite.organizationId);
- if (team) {
- await prisma.teamUser.create({
- data: {
- teamId,
- userId,
- role: isOwnerOrManager ? "admin" : "contributor",
- },
- });
+ if (!team) {
+ logger.warn({ teamId, userId }, "Team no longer exists during invite acceptance");
+ continue;
}
+
+ await prisma.teamUser.create({
+ data: {
+ teamId,
+ userId,
+ role: isOwnerOrManager ? "admin" : "contributor",
+ },
+ });
}
} catch (error) {
logger.error(error, `Error creating team membership ${invite.organizationId} ${userId}`);
@@ -39,7 +42,10 @@ export const createTeamMembership = async (invite: CreateMembershipInvite, userI
};
export const getTeamProjectIds = reactCache(
- async (teamId: string, organizationId: string): Promise<{ projectTeams: { projectId: string }[] }> => {
+ async (
+ teamId: string,
+ organizationId: string
+ ): Promise<{ projectTeams: { projectId: string }[] } | null> => {
const team = await prisma.team.findUnique({
where: {
id: teamId,
@@ -55,7 +61,7 @@ export const getTeamProjectIds = reactCache(
});
if (!team) {
- throw new Error("Team not found");
+ return null;
}
return team;
diff --git a/apps/web/modules/ee/role-management/components/add-member-role.tsx b/apps/web/modules/ee/role-management/components/add-member-role.tsx
index 2b5ed799d1..00400f3b5d 100644
--- a/apps/web/modules/ee/role-management/components/add-member-role.tsx
+++ b/apps/web/modules/ee/role-management/components/add-member-role.tsx
@@ -60,7 +60,7 @@ export function AddMemberRole({
name="role"
render={({ field: { onChange, value } }) => (
-
+
);
@@ -360,7 +382,7 @@ export const TeamSettingsModal = ({
: t("environments.settings.teams.all_members_added")
}>
)}
- setLeaveOrganizationModalOpen(false)}>
+ setIsLeaveOrganizationModalOpen(false)}>
{t("common.cancel")}
{
const ZFormSchema = z.object({
name: ZUserName,
email: z.string().min(1, { message: "Email is required" }).email({ message: "Invalid email" }),
role: ZOrganizationRole,
- teamIds: z.array(z.string()),
+ teamIds: showTeamAdminRestrictions
+ ? z.array(ZId).min(1, { message: "Team admins must select at least one team" })
+ : z.array(ZId),
});
const router = useRouter();
type TFormData = z.infer;
const { t } = useTranslation();
+
+ // Determine default role based on permissions
+ let defaultRole: TOrganizationRole = "owner";
+ if (showTeamAdminRestrictions || isAccessControlAllowed) {
+ defaultRole = "member";
+ }
+
const form = useForm({
resolver: zodResolver(ZFormSchema),
defaultValues: {
- role: isAccessControlAllowed ? "member" : "owner",
+ role: defaultRole,
teamIds: [],
},
});
@@ -104,43 +116,61 @@ export const IndividualInviteTab = ({
{errors.email && {errors.email.message}
}
-
- {watch("role") === "member" && (
-
- {t("environments.settings.teams.member_role_info_message")}
-
+ {showTeamAdminRestrictions ? (
+
+
+
+
+ ) : (
+ <>
+
+ {watch("role") === "member" && (
+
+
+ {t("environments.settings.teams.member_role_info_message")}
+
+
+ )}
+ >
)}
{isAccessControlAllowed && (
- (
-
- {t("common.add_to_team")}
-
- field.onChange(val)}
- />
- {!teamOptions.length && (
-
- {t("environments.settings.teams.create_first_team_message")}
-
- )}
-
-
- )}
- />
+ <>
+ (
+
+ {t("common.add_to_team")}
+
+ field.onChange(val)}
+ />
+ {!teamOptions.length && (
+
+ {t("environments.settings.teams.create_first_team_message")}
+
+ )}
+
+ {errors.teamIds?.message}
+
+ )}
+ />
+
+
+
+
+ >
)}
{!isAccessControlAllowed && (
diff --git a/apps/web/modules/organization/settings/teams/components/invite-member/invite-member-modal.tsx b/apps/web/modules/organization/settings/teams/components/invite-member/invite-member-modal.tsx
index 04c97b4d9b..3ecc3330ad 100644
--- a/apps/web/modules/organization/settings/teams/components/invite-member/invite-member-modal.tsx
+++ b/apps/web/modules/organization/settings/teams/components/invite-member/invite-member-modal.tsx
@@ -26,6 +26,9 @@ interface InviteMemberModalProps {
environmentId: string;
membershipRole?: TOrganizationRole;
isStorageConfigured: boolean;
+ isOwnerOrManager: boolean;
+ isTeamAdmin: boolean;
+ userAdminTeamIds?: string[];
}
export const InviteMemberModal = ({
@@ -38,11 +41,21 @@ export const InviteMemberModal = ({
environmentId,
membershipRole,
isStorageConfigured,
+ isOwnerOrManager,
+ isTeamAdmin,
+ userAdminTeamIds,
}: InviteMemberModalProps) => {
const [type, setType] = useState<"individual" | "bulk">("individual");
const { t } = useTranslation();
+ const showTeamAdminRestrictions = !isOwnerOrManager && isTeamAdmin;
+
+ const filteredTeams =
+ showTeamAdminRestrictions && userAdminTeamIds
+ ? teams.filter((t) => userAdminTeamIds.includes(t.id))
+ : teams;
+
const tabs = {
individual: (
),
bulk: (
@@ -75,16 +89,18 @@ export const InviteMemberModal = ({
- setType(inviteType)}
- defaultSelected={type}
- />
- {tabs[type]}
+ {!showTeamAdminRestrictions && (
+ setType(inviteType)}
+ defaultSelected={type}
+ />
+ )}
+ {showTeamAdminRestrictions ? tabs.individual : tabs[type]}
diff --git a/apps/web/modules/organization/settings/teams/components/members-view.tsx b/apps/web/modules/organization/settings/teams/components/members-view.tsx
index e745271966..cbae9e3e1c 100644
--- a/apps/web/modules/organization/settings/teams/components/members-view.tsx
+++ b/apps/web/modules/organization/settings/teams/components/members-view.tsx
@@ -5,6 +5,7 @@ import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/
import { INVITE_DISABLED, IS_FORMBRICKS_CLOUD, IS_STORAGE_CONFIGURED } from "@/lib/constants";
import { getTranslate } from "@/lingodotdev/server";
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
+import { getTeamsWhereUserIsAdmin } from "@/modules/ee/teams/lib/roles";
import { getTeamsByOrganizationId } from "@/modules/ee/teams/team-list/lib/team";
import { TOrganizationTeam } from "@/modules/ee/teams/team-list/types/team";
import { EditMemberships } from "@/modules/organization/settings/teams/components/edit-memberships";
@@ -45,6 +46,10 @@ export const MembersView = async ({
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
+ // Fetch admin teams if they're a team admin
+ const userAdminTeamIds = await getTeamsWhereUserIsAdmin(currentUserId, organization.id);
+ const isTeamAdminUser = userAdminTeamIds.length > 0;
+
let teams: TOrganizationTeam[] = [];
if (isAccessControlAllowed) {
@@ -69,6 +74,8 @@ export const MembersView = async ({
isMultiOrgEnabled={isMultiOrgEnabled}
teams={teams}
isUserManagementDisabledFromUi={isUserManagementDisabledFromUi}
+ isTeamAdmin={isTeamAdminUser}
+ userAdminTeamIds={userAdminTeamIds}
/>
)}
diff --git a/apps/web/modules/organization/settings/teams/page.tsx b/apps/web/modules/organization/settings/teams/page.tsx
index 2d534b80c9..9a5a45dea3 100644
--- a/apps/web/modules/organization/settings/teams/page.tsx
+++ b/apps/web/modules/organization/settings/teams/page.tsx
@@ -3,6 +3,7 @@ import { IS_FORMBRICKS_CLOUD, USER_MANAGEMENT_MINIMUM_ROLE } from "@/lib/constan
import { getUserManagementAccess } from "@/lib/membership/utils";
import { getTranslate } from "@/lingodotdev/server";
import { getAccessControlPermission } from "@/modules/ee/license-check/lib/utils";
+import { getTeamsWhereUserIsAdmin } from "@/modules/ee/teams/lib/roles";
import { TeamsView } from "@/modules/ee/teams/team-list/components/teams-view";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { MembersView } from "@/modules/organization/settings/teams/components/members-view";
@@ -16,11 +17,21 @@ export const TeamsPage = async (props) => {
const { session, currentUserMembership, organization } = await getEnvironmentAuth(params.environmentId);
const isAccessControlAllowed = await getAccessControlPermission(organization.billing.plan);
- const hasUserManagementAccess = getUserManagementAccess(
+
+ // Check if user has standard user management access (owner/manager)
+ const hasStandardUserManagementAccess = getUserManagementAccess(
currentUserMembership?.role,
USER_MANAGEMENT_MINIMUM_ROLE
);
+ // Also check if user is a team admin (they get limited user management for invites)
+ const userAdminTeamIds = await getTeamsWhereUserIsAdmin(session.user.id, organization.id);
+ const isTeamAdminUser = userAdminTeamIds.length > 0;
+
+ // Allow user management UI if they're owner/manager OR team admin (when access control is enabled)
+ const hasUserManagementAccess =
+ hasStandardUserManagementAccess || (isAccessControlAllowed && isTeamAdminUser);
+
return (
diff --git a/apps/web/modules/survey/link/components/link-survey-wrapper.tsx b/apps/web/modules/survey/link/components/link-survey-wrapper.tsx
index 29e52977a8..d5043d4862 100644
--- a/apps/web/modules/survey/link/components/link-survey-wrapper.tsx
+++ b/apps/web/modules/survey/link/components/link-survey-wrapper.tsx
@@ -79,7 +79,9 @@ export const LinkSurveyWrapper = ({
styling={styling}
onBackgroundLoaded={handleBackgroundLoaded}>
- {!styling.isLogoHidden && (project.logo?.url || styling.logo?.url) &&
}
+ {!styling.isLogoHidden && (project.logo?.url || styling.logo?.url) && (
+
+ )}
{isPreview && (
diff --git a/apps/web/modules/ui/components/client-logo/index.tsx b/apps/web/modules/ui/components/client-logo/index.tsx
index 51f768a94a..8b84264d47 100644
--- a/apps/web/modules/ui/components/client-logo/index.tsx
+++ b/apps/web/modules/ui/components/client-logo/index.tsx
@@ -15,7 +15,12 @@ interface ClientLogoProps {
previewSurvey?: boolean;
}
-export const ClientLogo = ({ environmentId, projectLogo, surveyLogo, previewSurvey = false }: ClientLogoProps) => {
+export const ClientLogo = ({
+ environmentId,
+ projectLogo,
+ surveyLogo,
+ previewSurvey = false,
+}: ClientLogoProps) => {
const { t } = useTranslation();
const logoToUse = surveyLogo?.url ? surveyLogo : projectLogo;
diff --git a/apps/web/modules/ui/components/select/index.tsx b/apps/web/modules/ui/components/select/index.tsx
index 3be9861226..b1079a0418 100644
--- a/apps/web/modules/ui/components/select/index.tsx
+++ b/apps/web/modules/ui/components/select/index.tsx
@@ -18,7 +18,7 @@ const SelectTrigger = React.forwardRef<
diff --git a/apps/web/playwright/organization.spec.ts b/apps/web/playwright/organization.spec.ts
index 18f99c2321..3d50b76613 100644
--- a/apps/web/playwright/organization.spec.ts
+++ b/apps/web/playwright/organization.spec.ts
@@ -20,7 +20,7 @@ test.describe("Invite, accept and remove organization member", async () => {
await page.locator('[data-testid="members-loading-card"]:first-child').waitFor({ state: "hidden" });
- await page.getByRole("link", { name: "Access Control" }).click();
+ await page.getByRole("link", { name: "Members & Teams" }).click();
// Add member button
await expect(page.getByRole("button", { name: "Invite member" })).toBeVisible();
@@ -131,8 +131,8 @@ test.describe("Create, update and delete team", async () => {
await page.waitForURL(/\/environments\/[^/]+\/settings\/general/);
await page.waitForTimeout(2000);
- await expect(page.getByText("Access Control")).toBeVisible();
- await page.getByText("Access Control").click();
+ await expect(page.getByText("Members & Teams")).toBeVisible();
+ await page.getByText("Members & Teams").click();
await page.waitForURL(/\/environments\/[^/]+\/settings\/teams/);
await expect(page.getByRole("button", { name: "Create new team" })).toBeVisible();
await page.getByRole("button", { name: "Create new team" }).click();
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e3d3f5677f..bc83c3f7ca 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7754,6 +7754,7 @@ packages:
next@15.5.7:
resolution: {integrity: sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
+ deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0