mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-12 03:09:41 -06:00
chore: improve UX in team settings
This commit is contained in:
@@ -1030,6 +1030,8 @@ checksums:
|
||||
environments/settings/teams/manage_team_disabled: 2aaa0557b403a5bc657ec9e8b19ac5ac
|
||||
environments/settings/teams/manager_role_description: 020b8674cc330536239dafe5db087de6
|
||||
environments/settings/teams/member: 1606dc30b369856b9dba1fe9aec425d2
|
||||
environments/settings/teams/member_added_to_team: 82bd83175a2434b1cbe87a6550b0a956
|
||||
environments/settings/teams/member_in_all_teams: df051e8db2dcb142948e7fc44ded10c3
|
||||
environments/settings/teams/member_role_description: 8c2447cb929850b41619988be0b9b1ac
|
||||
environments/settings/teams/member_role_info_message: 511f3b954486f9c8a3b043e498f21c69
|
||||
environments/settings/teams/organization_role: 979b75fcc3696952e5922d659c839c10
|
||||
@@ -1038,8 +1040,6 @@ checksums:
|
||||
environments/settings/teams/please_fill_all_workspace_fields: 190fc5d3c63cc5ec49d77f587e619ed8
|
||||
environments/settings/teams/read: 2494ca23d10e5b6381eb271aceeb5270
|
||||
environments/settings/teams/read_write: 278a90dade128198d4c93ac00c345320
|
||||
environments/settings/teams/select_member: 7f4a38312aabbbe3fe92756b57bd5d75
|
||||
environments/settings/teams/select_workspace: 0ad989c23616c6a04faf23d9e63ed3f3
|
||||
environments/settings/teams/team_admin: 5df68214685738029af678ae1d5912bb
|
||||
environments/settings/teams/team_created_successfully: 5b0cc007e18053508fdebc9545cc2c05
|
||||
environments/settings/teams/team_deleted_successfully: d0729ad8d982cc5d542f89291bf57c50
|
||||
|
||||
@@ -1097,6 +1097,8 @@
|
||||
"manage_team_disabled": "Nur Organisationsbesitzer, Manager und Team-Admins können Teams verwalten.",
|
||||
"manager_role_description": "Manager können auf alle Workspaces zugreifen und Mitglieder hinzufügen oder entfernen.",
|
||||
"member": "Mitglied",
|
||||
"member_added_to_team": "Mitglied erfolgreich zum Team hinzugefügt",
|
||||
"member_in_all_teams": "Mitglied ist bereits in allen Teams.",
|
||||
"member_role_description": "Mitglieder können in ausgewählten Workspaces arbeiten.",
|
||||
"member_role_info_message": "Um neuen Mitgliedern Zugriff auf einen Workspace zu gewähren, fügen Sie sie bitte unten einem Team hinzu. Mit Teams können Sie verwalten, wer Zugriff auf welchen Workspace hat.",
|
||||
"organization_role": "Organisationsrolle",
|
||||
@@ -1105,8 +1107,6 @@
|
||||
"please_fill_all_workspace_fields": "Bitte füllen Sie alle Felder aus, um einen neuen Workspace hinzuzufügen.",
|
||||
"read": "Lesen",
|
||||
"read_write": "Lesen & Schreiben",
|
||||
"select_member": "Mitglied auswählen",
|
||||
"select_workspace": "Workspace auswählen",
|
||||
"team_admin": "Team-Admin",
|
||||
"team_created_successfully": "Team erfolgreich erstellt.",
|
||||
"team_deleted_successfully": "Team erfolgreich gelöscht.",
|
||||
|
||||
@@ -1097,6 +1097,8 @@
|
||||
"manage_team_disabled": "Only organization owners, managers and team admins can manage teams.",
|
||||
"manager_role_description": "Managers can access all workspaces and add and remove members.",
|
||||
"member": "Member",
|
||||
"member_added_to_team": "Member added to team successfully",
|
||||
"member_in_all_teams": "Member is already in all teams.",
|
||||
"member_role_description": "Members can work in selected workspaces.",
|
||||
"member_role_info_message": "To give new members access to a workspace, please add them to a Team below. With Teams you can manage who has access to which workspace.",
|
||||
"organization_role": "Organization role",
|
||||
@@ -1105,8 +1107,6 @@
|
||||
"please_fill_all_workspace_fields": "Please fill all the fields to add a new workspace.",
|
||||
"read": "Read",
|
||||
"read_write": "Read & Write",
|
||||
"select_member": "Select member",
|
||||
"select_workspace": "Select workspace",
|
||||
"team_admin": "Team Admin",
|
||||
"team_created_successfully": "Team created successfully",
|
||||
"team_deleted_successfully": "Team deleted successfully",
|
||||
@@ -1156,7 +1156,6 @@
|
||||
"add_fallback_placeholder": "Add a placeholder to show if there is no value to recall.",
|
||||
"add_hidden_field_id": "Add hidden field ID",
|
||||
"add_highlight_border": "Add highlight border",
|
||||
|
||||
"add_logic": "Add logic",
|
||||
"add_none_of_the_above": "Add “None of the Above”",
|
||||
"add_option": "Add option",
|
||||
@@ -1230,11 +1229,8 @@
|
||||
"change_background": "Change background",
|
||||
"change_question_type": "Change question type",
|
||||
"change_survey_type": "Switching survey type affects existing access",
|
||||
|
||||
"change_the_background_to_a_color_image_or_animation": "Change the background to a color, image or animation.",
|
||||
|
||||
"change_the_placement_of_this_survey": "Change the placement of this survey.",
|
||||
|
||||
"changes_saved": "Changes saved.",
|
||||
"changing_survey_type_will_remove_existing_distribution_channels": "Changing the survey type will affect how it can be shared. If respondents already have access links for the current type, they may lose access after the switch.",
|
||||
"checkbox_label": "Checkbox Label",
|
||||
@@ -1374,7 +1370,6 @@
|
||||
"hide_progress_bar": "Hide progress bar",
|
||||
"hide_question_settings": "Hide Question settings",
|
||||
"hostname": "Hostname",
|
||||
|
||||
"if_you_need_more_please": "If you need more, please",
|
||||
"if_you_really_want_that_answer_ask_until_you_get_it": "Keep showing whenever triggered until a response is submitted.",
|
||||
"ignore_global_waiting_time": "Ignore Cooldown Period",
|
||||
@@ -1471,7 +1466,6 @@
|
||||
"protect_survey_with_pin_description": "Only users who have the PIN can access the survey.",
|
||||
"publish": "Publish",
|
||||
"question": "Question",
|
||||
|
||||
"question_deleted": "Question deleted.",
|
||||
"question_duplicated": "Question duplicated.",
|
||||
"question_id_updated": "Question ID updated",
|
||||
@@ -1575,7 +1569,6 @@
|
||||
"styling_set_to_theme_styles": "Styling set to theme styles",
|
||||
"subheading": "Subheading",
|
||||
"subtract": "Subtract -",
|
||||
|
||||
"survey_completed_heading": "Survey Completed",
|
||||
"survey_completed_subheading": "This free & open-source survey has been closed",
|
||||
"survey_display_settings": "Survey Display Settings",
|
||||
@@ -2059,9 +2052,7 @@
|
||||
"look": {
|
||||
"add_background_color": "Add background color",
|
||||
"add_background_color_description": "Add a background color to the logo container.",
|
||||
|
||||
"advanced_styling_field_border_radius": "Border Radius",
|
||||
|
||||
"advanced_styling_field_button_bg": "Button Background",
|
||||
"advanced_styling_field_button_bg_description": "Fills the Next / Submit button.",
|
||||
"advanced_styling_field_button_border_radius_description": "Rounds the button corners.",
|
||||
@@ -2078,7 +2069,6 @@
|
||||
"advanced_styling_field_description_size_description": "Scales the description text.",
|
||||
"advanced_styling_field_description_weight": "Description Font Weight",
|
||||
"advanced_styling_field_description_weight_description": "Makes description text lighter or bolder.",
|
||||
|
||||
"advanced_styling_field_font_size": "Font Size",
|
||||
"advanced_styling_field_font_weight": "Font Weight",
|
||||
"advanced_styling_field_headline_color": "Headline Color",
|
||||
@@ -2090,7 +2080,6 @@
|
||||
"advanced_styling_field_height": "Height",
|
||||
"advanced_styling_field_indicator_bg": "Indicator Background",
|
||||
"advanced_styling_field_indicator_bg_description": "Colors the filled portion of the bar.",
|
||||
|
||||
"advanced_styling_field_input_border_radius_description": "Rounds the input corners.",
|
||||
"advanced_styling_field_input_font_size_description": "Scales the typed text in inputs.",
|
||||
"advanced_styling_field_input_height_description": "Controls the input field height.",
|
||||
@@ -2116,7 +2105,6 @@
|
||||
"advanced_styling_field_track_bg_description": "Colors the unfilled portion of the bar.",
|
||||
"advanced_styling_field_track_height": "Track Height",
|
||||
"advanced_styling_field_track_height_description": "Controls the progress bar thickness.",
|
||||
|
||||
"advanced_styling_field_upper_label_color": "Headline Label Color",
|
||||
"advanced_styling_field_upper_label_color_description": "Colors the small label above inputs.",
|
||||
"advanced_styling_field_upper_label_size": "Headline Label Font Size",
|
||||
@@ -2127,7 +2115,6 @@
|
||||
"advanced_styling_section_headlines": "Headlines & Descriptions",
|
||||
"advanced_styling_section_inputs": "Inputs",
|
||||
"advanced_styling_section_options": "Options (Radio/Checkbox)",
|
||||
|
||||
"app_survey_placement": "App Survey Placement",
|
||||
"app_survey_placement_settings_description": "Change where surveys will be shown in your web app or website.",
|
||||
"email_customization": "Email Customization",
|
||||
|
||||
@@ -1097,6 +1097,8 @@
|
||||
"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_added_to_team": "Miembro añadido al equipo correctamente",
|
||||
"member_in_all_teams": "El miembro ya está en todos los equipos.",
|
||||
"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",
|
||||
@@ -1105,8 +1107,6 @@
|
||||
"please_fill_all_workspace_fields": "Por favor, rellena todos los campos para añadir un proyecto nuevo.",
|
||||
"read": "Lectura",
|
||||
"read_write": "Lectura y escritura",
|
||||
"select_member": "Seleccionar miembro",
|
||||
"select_workspace": "Seleccionar proyecto",
|
||||
"team_admin": "Administrador de equipo",
|
||||
"team_created_successfully": "Equipo creado con éxito.",
|
||||
"team_deleted_successfully": "Equipo eliminado correctamente.",
|
||||
|
||||
@@ -1097,6 +1097,8 @@
|
||||
"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 espaces de travail et ajouter ou supprimer des membres.",
|
||||
"member": "Membre",
|
||||
"member_added_to_team": "Membre ajouté à l'équipe avec succès",
|
||||
"member_in_all_teams": "Le membre est déjà dans toutes les équipes.",
|
||||
"member_role_description": "Les membres peuvent travailler dans les espaces de travail sélectionnés.",
|
||||
"member_role_info_message": "Pour donner aux nouveaux membres l'accès à un espace de travail, veuillez les ajouter à une équipe ci-dessous. Avec les équipes, vous pouvez gérer qui a accès à quel espace de travail.",
|
||||
"organization_role": "Rôle dans l'organisation",
|
||||
@@ -1105,8 +1107,6 @@
|
||||
"please_fill_all_workspace_fields": "Veuillez remplir tous les champs pour ajouter un nouvel espace de travail.",
|
||||
"read": "Lire",
|
||||
"read_write": "Lire et Écrire",
|
||||
"select_member": "Sélectionner membre",
|
||||
"select_workspace": "Sélectionner un espace de travail",
|
||||
"team_admin": "Administrateur d'équipe",
|
||||
"team_created_successfully": "Équipe créée avec succès.",
|
||||
"team_deleted_successfully": "Équipe supprimée avec succès.",
|
||||
|
||||
@@ -1097,6 +1097,8 @@
|
||||
"manage_team_disabled": "Csak a szervezet tulajdonosai, kezelői és csapatadminisztrátorai kezelhetnek csapatokat.",
|
||||
"manager_role_description": "A kezelők hozzáférhetnek az összes munkaterülethez, valamint tagokat adhatnak hozzá és távolíthatnak el.",
|
||||
"member": "Tag",
|
||||
"member_added_to_team": "Tag sikeresen hozzáadva a csapathoz",
|
||||
"member_in_all_teams": "A tag már minden csapatban benne van.",
|
||||
"member_role_description": "A tagok a kiválasztott munkaterületeken dolgozhatnak.",
|
||||
"member_role_info_message": "Ahhoz, hogy az új tagoknak hozzáférést adjon egy munkaterülethez, adja hozzá őket lent egy csapathoz. A csapatokkal kezelheti azt, hogy kinek melyik munkaterülethez van hozzáférése.",
|
||||
"organization_role": "Szervezeti szerep",
|
||||
@@ -1105,8 +1107,6 @@
|
||||
"please_fill_all_workspace_fields": "Töltse ki az összes mezőt egy új munkaterület hozzáadásához.",
|
||||
"read": "Olvasás",
|
||||
"read_write": "Olvasás és írás",
|
||||
"select_member": "Tag kiválasztása",
|
||||
"select_workspace": "Munkaterület kiválasztása",
|
||||
"team_admin": "Csapatadminisztrátor",
|
||||
"team_created_successfully": "A csapat sikeresen létrehozva",
|
||||
"team_deleted_successfully": "A csapat sikeresen törölve",
|
||||
|
||||
@@ -1097,6 +1097,8 @@
|
||||
"manage_team_disabled": "組織のオーナー、管理者、チーム管理者のみがチームを管理できます。",
|
||||
"manager_role_description": "マネージャーはすべてのワークスペースにアクセスでき、メンバーの追加と削除が可能です。",
|
||||
"member": "メンバー",
|
||||
"member_added_to_team": "メンバーがチームに正常に追加されました",
|
||||
"member_in_all_teams": "メンバーはすでにすべてのチームに参加しています。",
|
||||
"member_role_description": "メンバーは選択されたワークスペースで作業できます。",
|
||||
"member_role_info_message": "新しいメンバーにワークスペースへのアクセス権を付与するには、以下のチームに追加してください。チームを使用すると、誰がどのワークスペースにアクセスできるかを管理できます。",
|
||||
"organization_role": "組織の役割",
|
||||
@@ -1105,8 +1107,6 @@
|
||||
"please_fill_all_workspace_fields": "新しいワークスペースを追加するには、すべてのフィールドを入力してください。",
|
||||
"read": "読み取り",
|
||||
"read_write": "読み書き",
|
||||
"select_member": "メンバーを選択",
|
||||
"select_workspace": "ワークスペースを選択",
|
||||
"team_admin": "チーム管理者",
|
||||
"team_created_successfully": "チームを正常に作成しました。",
|
||||
"team_deleted_successfully": "チームを正常に削除しました。",
|
||||
|
||||
@@ -1097,6 +1097,8 @@
|
||||
"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_added_to_team": "Lid succesvol toegevoegd aan team",
|
||||
"member_in_all_teams": "Lid zit al in alle teams.",
|
||||
"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",
|
||||
@@ -1105,8 +1107,6 @@
|
||||
"please_fill_all_workspace_fields": "Vul alle velden in om een nieuwe werkruimte toe te voegen.",
|
||||
"read": "Lezen",
|
||||
"read_write": "Lezen en schrijven",
|
||||
"select_member": "Selecteer lid",
|
||||
"select_workspace": "Selecteer werkruimte",
|
||||
"team_admin": "Teambeheerder",
|
||||
"team_created_successfully": "Team succesvol aangemaakt.",
|
||||
"team_deleted_successfully": "Team succesvol verwijderd.",
|
||||
|
||||
@@ -1097,6 +1097,8 @@
|
||||
"manage_team_disabled": "Apenas proprietários da organização, gerentes e administradores da equipe podem gerenciar equipes.",
|
||||
"manager_role_description": "Gerentes podem acessar todos os espaços de trabalho e adicionar e remover membros.",
|
||||
"member": "Membro",
|
||||
"member_added_to_team": "Membro adicionado à equipe com sucesso",
|
||||
"member_in_all_teams": "O membro já está em todas as equipes.",
|
||||
"member_role_description": "Membros podem trabalhar em espaços de trabalho selecionados.",
|
||||
"member_role_info_message": "Para dar acesso aos novos membros a um espaço de trabalho, adicione-os a uma equipe abaixo. Com as equipes você pode gerenciar quem tem acesso a qual espaço de trabalho.",
|
||||
"organization_role": "Função na organização",
|
||||
@@ -1105,8 +1107,6 @@
|
||||
"please_fill_all_workspace_fields": "Preencha todos os campos para adicionar um novo espaço de trabalho.",
|
||||
"read": "Leitura",
|
||||
"read_write": "Leitura & Escrita",
|
||||
"select_member": "Selecionar membro",
|
||||
"select_workspace": "Selecionar espaço de trabalho",
|
||||
"team_admin": "Administrador da equipe",
|
||||
"team_created_successfully": "Equipe criada com sucesso.",
|
||||
"team_deleted_successfully": "Equipe excluída com sucesso.",
|
||||
|
||||
@@ -1097,6 +1097,8 @@
|
||||
"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 espaços de trabalho e adicionar e remover membros.",
|
||||
"member": "Membro",
|
||||
"member_added_to_team": "Membro adicionado à equipa com sucesso",
|
||||
"member_in_all_teams": "O membro já está em todas as equipas.",
|
||||
"member_role_description": "Os membros podem trabalhar em espaços de trabalho selecionados.",
|
||||
"member_role_info_message": "Para dar acesso aos novos membros a um espaço de trabalho, adicione-os a uma equipa abaixo. Com as equipas pode gerir quem tem acesso a cada espaço de trabalho.",
|
||||
"organization_role": "Função na organização",
|
||||
@@ -1105,8 +1107,6 @@
|
||||
"please_fill_all_workspace_fields": "Preencha todos os campos para adicionar um novo espaço de trabalho.",
|
||||
"read": "Ler",
|
||||
"read_write": "Ler e Escrever",
|
||||
"select_member": "Selecionar membro",
|
||||
"select_workspace": "Selecionar espaço de trabalho",
|
||||
"team_admin": "Administrador da Equipa",
|
||||
"team_created_successfully": "Equipa criada com sucesso.",
|
||||
"team_deleted_successfully": "Equipa eliminada com sucesso.",
|
||||
|
||||
@@ -1097,6 +1097,8 @@
|
||||
"manage_team_disabled": "Doar proprietarii de organizații, managerii și administratorii de echipă pot gestiona echipele.",
|
||||
"manager_role_description": "Managerii pot accesa toate spațiile de lucru și pot adăuga sau elimina membri.",
|
||||
"member": "Membru",
|
||||
"member_added_to_team": "Membrul a fost adăugat cu succes în echipă",
|
||||
"member_in_all_teams": "Membrul este deja în toate echipele.",
|
||||
"member_role_description": "Membrii pot lucra în spațiile de lucru selectate.",
|
||||
"member_role_info_message": "Pentru a oferi noilor membri acces la un spațiu de lucru, adăugați-i mai jos într-o echipă. Cu ajutorul echipelor puteți gestiona cine are acces la fiecare spațiu de lucru.",
|
||||
"organization_role": "Rol în organizație",
|
||||
@@ -1105,8 +1107,6 @@
|
||||
"please_fill_all_workspace_fields": "Vă rugăm să completați toate câmpurile pentru a adăuga un nou spațiu de lucru.",
|
||||
"read": "Citește",
|
||||
"read_write": "Citire & Scriere",
|
||||
"select_member": "Selectează membrul",
|
||||
"select_workspace": "Selectați spațiul de lucru",
|
||||
"team_admin": "Administrator Echipe",
|
||||
"team_created_successfully": "Echipă creată cu succes",
|
||||
"team_deleted_successfully": "Echipă ștearsă cu succes.",
|
||||
|
||||
@@ -1097,6 +1097,8 @@
|
||||
"manage_team_disabled": "Только владельцы организации, менеджеры и администраторы команд могут управлять командами.",
|
||||
"manager_role_description": "Менеджеры имеют доступ ко всем проектам, а также могут добавлять и удалять участников.",
|
||||
"member": "Участник",
|
||||
"member_added_to_team": "Участник успешно добавлен в команду",
|
||||
"member_in_all_teams": "Участник уже состоит во всех командах.",
|
||||
"member_role_description": "Участники могут работать в выбранных проектах.",
|
||||
"member_role_info_message": "Чтобы предоставить новым участникам доступ к проекту, добавьте их в команду ниже. С помощью команд вы можете управлять доступом к проектам.",
|
||||
"organization_role": "Роль в организации",
|
||||
@@ -1105,8 +1107,6 @@
|
||||
"please_fill_all_workspace_fields": "Пожалуйста, заполните все поля для добавления нового рабочего пространства.",
|
||||
"read": "Чтение",
|
||||
"read_write": "Чтение и запись",
|
||||
"select_member": "Выберите участника",
|
||||
"select_workspace": "Выберите рабочее пространство",
|
||||
"team_admin": "Администратор команды",
|
||||
"team_created_successfully": "Команда успешно создана.",
|
||||
"team_deleted_successfully": "Команда успешно удалена.",
|
||||
|
||||
@@ -1097,6 +1097,8 @@
|
||||
"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_added_to_team": "Medlem har lagts till i teamet",
|
||||
"member_in_all_teams": "Medlemmen är redan med i alla team.",
|
||||
"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",
|
||||
@@ -1105,8 +1107,6 @@
|
||||
"please_fill_all_workspace_fields": "Fyll i alla fält för att lägga till en ny arbetsyta.",
|
||||
"read": "Läs",
|
||||
"read_write": "Läs och skriv",
|
||||
"select_member": "Välj medlem",
|
||||
"select_workspace": "Välj arbetsyta",
|
||||
"team_admin": "Teamadministratör",
|
||||
"team_created_successfully": "Team skapat.",
|
||||
"team_deleted_successfully": "Team borttaget.",
|
||||
|
||||
@@ -1097,6 +1097,8 @@
|
||||
"manage_team_disabled": "只有 组织 拥有者、经理 和 团队 管理员 可以 管理 团队。",
|
||||
"manager_role_description": "经理 可以 访问 所有 项目 并 添加 移除 成员。",
|
||||
"member": "成员",
|
||||
"member_added_to_team": "成员已成功加入团队",
|
||||
"member_in_all_teams": "该成员已在所有团队中。",
|
||||
"member_role_description": "成员 可以 在 选定 项目 中 工作。",
|
||||
"member_role_info_message": "要 给 新 成员 访问 项目 ,请 将 他们 添加 到 下方 的 团队 。通过 团队 你 可以 管理 谁 可以 访问 哪个 项目 。",
|
||||
"organization_role": "组织角色",
|
||||
@@ -1105,8 +1107,6 @@
|
||||
"please_fill_all_workspace_fields": "请填写所有字段以添加新工作区。",
|
||||
"read": "阅读",
|
||||
"read_write": "读 & 写",
|
||||
"select_member": "选择成员",
|
||||
"select_workspace": "选择工作区",
|
||||
"team_admin": "团队管理员",
|
||||
"team_created_successfully": "团队 创建 成功",
|
||||
"team_deleted_successfully": "团队 删除 成功",
|
||||
|
||||
@@ -1097,6 +1097,8 @@
|
||||
"manage_team_disabled": "只有組織擁有者、管理員和團隊管理員才能管理團隊。",
|
||||
"manager_role_description": "管理員可以存取所有專案,並新增和移除成員。",
|
||||
"member": "成員",
|
||||
"member_added_to_team": "已成功將成員加入團隊",
|
||||
"member_in_all_teams": "該成員已在所有團隊中。",
|
||||
"member_role_description": "成員可以在選定的專案中工作。",
|
||||
"member_role_info_message": "若要授予新成員存取專案的權限,請將他們新增至下方的團隊。藉由團隊,您可以管理誰可以存取哪些專案。",
|
||||
"organization_role": "組織角色",
|
||||
@@ -1105,8 +1107,6 @@
|
||||
"please_fill_all_workspace_fields": "請填寫所有欄位以新增工作區。",
|
||||
"read": "讀取",
|
||||
"read_write": "讀取和寫入",
|
||||
"select_member": "選擇成員",
|
||||
"select_workspace": "選擇工作區",
|
||||
"team_admin": "團隊管理員",
|
||||
"team_created_successfully": "團隊已成功建立。",
|
||||
"team_deleted_successfully": "團隊已成功刪除。",
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { InvalidInputError, ResourceNotFoundError, UnknownError } from "@formbricks/types/errors";
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
|
||||
@@ -161,3 +163,88 @@ export const getTeamRoleAction = authenticatedActionClient
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
return await getTeamRoleByTeamIdUserId(parsedInput.teamId, ctx.user.id);
|
||||
});
|
||||
|
||||
const ZAddMemberToTeamAction = z.object({
|
||||
teamId: ZId,
|
||||
userId: ZId,
|
||||
role: z.enum(["admin", "contributor"]).default("contributor"),
|
||||
});
|
||||
|
||||
export const addMemberToTeamAction = authenticatedActionClient.schema(ZAddMemberToTeamAction).action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"team",
|
||||
async ({
|
||||
ctx,
|
||||
parsedInput,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZAddMemberToTeamAction>;
|
||||
}) => {
|
||||
const organizationId = await getOrganizationIdFromTeamId(parsedInput.teamId);
|
||||
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "team",
|
||||
teamId: parsedInput.teamId,
|
||||
minPermission: "admin",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await checkRoleManagementPermission(organizationId);
|
||||
|
||||
const teamDetails = await getTeamDetails(parsedInput.teamId);
|
||||
if (!teamDetails) {
|
||||
throw new ResourceNotFoundError("Team", parsedInput.teamId);
|
||||
}
|
||||
|
||||
if (teamDetails.projects.length === 0) {
|
||||
throw new InvalidInputError("Please add at least one workspace to the team " + teamDetails.name);
|
||||
}
|
||||
|
||||
const isAlreadyMember = teamDetails.members.some((m) => m.userId === parsedInput.userId);
|
||||
if (isAlreadyMember) {
|
||||
return { data: true };
|
||||
}
|
||||
|
||||
// Owners and managers are always team admins, never contributors
|
||||
const userMembership = await getMembershipByUserIdOrganizationId(parsedInput.userId, organizationId);
|
||||
const isOwnerOrManager = userMembership?.role === "owner" || userMembership?.role === "manager";
|
||||
const teamRole = isOwnerOrManager ? "admin" : parsedInput.role;
|
||||
|
||||
const updatedMembers = [
|
||||
...teamDetails.members.map((m) => ({ userId: m.userId, role: m.role })),
|
||||
{ userId: parsedInput.userId, role: teamRole },
|
||||
];
|
||||
|
||||
const updatedProjects = teamDetails.projects.map((p) => ({
|
||||
projectId: p.projectId,
|
||||
permission: p.permission,
|
||||
}));
|
||||
|
||||
const result = await updateTeamDetails(parsedInput.teamId, {
|
||||
name: teamDetails.name,
|
||||
members: updatedMembers,
|
||||
projects: updatedProjects,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new UnknownError("Failed to add member to team");
|
||||
}
|
||||
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.teamId = parsedInput.teamId;
|
||||
ctx.auditLoggingCtx.oldObject = teamDetails;
|
||||
ctx.auditLoggingCtx.newObject = await getTeamDetails(parsedInput.teamId);
|
||||
return { data: true };
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import { XIcon } from "lucide-react";
|
||||
import { Control } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
TOrganizationMember,
|
||||
TTeamRole,
|
||||
TTeamSettingsFormSchema,
|
||||
ZTeamRole,
|
||||
} from "@/modules/ee/teams/team-list/types/team";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { FormError, FormField, FormItem } from "@/modules/ui/components/form";
|
||||
import { InputCombobox } from "@/modules/ui/components/input-combo-box";
|
||||
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||
|
||||
export interface MemberRowProps {
|
||||
index: number;
|
||||
member: { userId: string; role: TTeamRole };
|
||||
memberOpts: { value: string; label: string }[];
|
||||
control: Control<TTeamSettingsFormSchema>;
|
||||
orgMembers: TOrganizationMember[];
|
||||
watchMembers: { userId: string; role: TTeamRole }[];
|
||||
initialMemberIds: Set<string>;
|
||||
isOwnerOrManager: boolean;
|
||||
isTeamAdminMember: boolean;
|
||||
isTeamContributorMember: boolean;
|
||||
currentUserId: string;
|
||||
onMemberSelectionChange: (index: number, userId: string) => void;
|
||||
onRemoveMember: (index: number) => void;
|
||||
memberCount: number;
|
||||
}
|
||||
|
||||
export function MemberRow(props: Readonly<MemberRowProps>) {
|
||||
const {
|
||||
index,
|
||||
member,
|
||||
memberOpts,
|
||||
control,
|
||||
orgMembers,
|
||||
watchMembers,
|
||||
initialMemberIds,
|
||||
isOwnerOrManager,
|
||||
isTeamAdminMember,
|
||||
isTeamContributorMember,
|
||||
currentUserId,
|
||||
onMemberSelectionChange,
|
||||
onRemoveMember,
|
||||
memberCount,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
const chosenMember = orgMembers.find((m) => m.id === watchMembers[index]?.userId);
|
||||
const canEditWhenNoMember = isOwnerOrManager || isTeamAdminMember;
|
||||
const isRoleSelectDisabled =
|
||||
chosenMember === undefined
|
||||
? !canEditWhenNoMember
|
||||
: chosenMember.role === "owner" ||
|
||||
chosenMember.role === "manager" ||
|
||||
isTeamContributorMember ||
|
||||
chosenMember.id === currentUserId;
|
||||
|
||||
return (
|
||||
<div className="flex gap-2.5">
|
||||
<FormField
|
||||
control={control}
|
||||
name={`members.${index}.userId`}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
const isExistingMember = member.userId && initialMemberIds.has(member.userId);
|
||||
const isSelectDisabled = isExistingMember || (!isOwnerOrManager && !isTeamAdminMember);
|
||||
|
||||
return (
|
||||
<FormItem className="flex-1">
|
||||
<div className={isSelectDisabled ? "pointer-events-none opacity-50" : undefined}>
|
||||
<InputCombobox
|
||||
id={`member-select-${index}`}
|
||||
options={memberOpts}
|
||||
value={field.value || null}
|
||||
onChangeValue={(val) => {
|
||||
const value = typeof val === "string" ? val : "";
|
||||
field.onChange(value);
|
||||
onMemberSelectionChange(index, value);
|
||||
}}
|
||||
showSearch
|
||||
searchPlaceholder={t("common.search")}
|
||||
comboboxClasses="flex-1 min-w-0 w-full"
|
||||
emptyDropdownText={t("environments.surveys.edit.no_option_found")}
|
||||
/>
|
||||
</div>
|
||||
{error?.message && <FormError className="text-left">{error.message}</FormError>}
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name={`members.${index}.role`}
|
||||
render={({ field }) => {
|
||||
const roleOptions = [
|
||||
{ value: ZTeamRole.enum.admin, label: t("environments.settings.teams.team_admin") },
|
||||
{
|
||||
value: ZTeamRole.enum.contributor,
|
||||
label: t("environments.settings.teams.contributor"),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<FormItem className="flex-1">
|
||||
<div className={isRoleSelectDisabled ? "pointer-events-none opacity-50" : undefined}>
|
||||
<InputCombobox
|
||||
id={`member-role-select-${index}`}
|
||||
options={roleOptions}
|
||||
value={field.value}
|
||||
onChangeValue={(val) => field.onChange(val)}
|
||||
showSearch={false}
|
||||
comboboxClasses="flex-1 min-w-0 w-full"
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{memberCount > 1 && (
|
||||
<TooltipRenderer tooltipContent={t("common.remove_from_team")}>
|
||||
<Button
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="destructive"
|
||||
className="shrink-0"
|
||||
disabled={!isOwnerOrManager && (!isTeamAdminMember || member.userId === currentUserId)}
|
||||
onClick={() => onRemoveMember(index)}>
|
||||
<XIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PlusIcon, Trash2Icon, XIcon } from "lucide-react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo } from "react";
|
||||
import { FormProvider, SubmitHandler, useForm, useWatch } from "react-hook-form";
|
||||
@@ -13,6 +13,8 @@ import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { ZTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
|
||||
import { updateTeamDetailsAction } from "@/modules/ee/teams/team-list/actions";
|
||||
import { DeleteTeam } from "@/modules/ee/teams/team-list/components/team-settings/delete-team";
|
||||
import { MemberRow } from "@/modules/ee/teams/team-list/components/team-settings/member-row";
|
||||
import { WorkspaceRow } from "@/modules/ee/teams/team-list/components/team-settings/workspace-row";
|
||||
import { TOrganizationProject } from "@/modules/ee/teams/team-list/types/project";
|
||||
import {
|
||||
TOrganizationMember,
|
||||
@@ -36,13 +38,6 @@ import {
|
||||
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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/modules/ui/components/select";
|
||||
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||
import { Muted } from "@/modules/ui/components/typography";
|
||||
|
||||
@@ -187,14 +182,16 @@ export const TeamSettingsModal = ({
|
||||
const currentMemberId = watchMembers[index]?.userId;
|
||||
return orgMembers
|
||||
.filter((om) => !selectedMemberIds.includes(om?.id) || om?.id === currentMemberId)
|
||||
.map((om) => ({ label: om?.name, value: om?.id }));
|
||||
.map((om) => ({ label: om?.name ?? "", value: om?.id }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
|
||||
};
|
||||
|
||||
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 }));
|
||||
.map((op) => ({ label: op?.name ?? "", value: op?.id }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
|
||||
};
|
||||
|
||||
const handleMemberSelectionChange = (index: number, userId: string) => {
|
||||
@@ -262,110 +259,25 @@ export const TeamSettingsModal = ({
|
||||
render={({ fieldState: { error } }) => (
|
||||
<FormItem className="flex-1">
|
||||
<div className="space-y-2 overflow-y-auto">
|
||||
{watchMembers.map((member, index) => {
|
||||
const memberOpts = getMemberOptionsForIndex(index);
|
||||
return (
|
||||
<div key={`member-${member.userId}-${index}`} className="flex gap-2.5">
|
||||
<FormField
|
||||
control={control}
|
||||
name={`members.${index}.userId`}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
// Disable user select for existing members (can only remove or change role)
|
||||
const isExistingMember =
|
||||
member.userId && initialMemberIds.has(member.userId);
|
||||
const isSelectDisabled =
|
||||
isExistingMember || (!isOwnerOrManager && !isTeamAdminMember);
|
||||
|
||||
return (
|
||||
<FormItem className="flex-1">
|
||||
<Select
|
||||
onValueChange={(val) => {
|
||||
field.onChange(val);
|
||||
handleMemberSelectionChange(index, val);
|
||||
}}
|
||||
disabled={isSelectDisabled}
|
||||
value={member.userId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t("environments.settings.teams.select_member")}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{memberOpts.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
id={`member-${index}-option`}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{error?.message && (
|
||||
<FormError className="text-left">{error.message}</FormError>
|
||||
)}
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={control}
|
||||
name={`members.${index}.role`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={member.role}
|
||||
disabled={(() => {
|
||||
const chosenMember = orgMembers.find(
|
||||
(m) => m.id === watchMembers[index]?.userId
|
||||
);
|
||||
if (!chosenMember) return !isOwnerOrManager && !isTeamAdminMember;
|
||||
|
||||
return (
|
||||
chosenMember.role === "owner" ||
|
||||
chosenMember.role === "manager" ||
|
||||
isTeamContributorMember ||
|
||||
chosenMember.id === currentUserId
|
||||
);
|
||||
})()}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ZTeamRole.enum.admin}>
|
||||
{t("environments.settings.teams.team_admin")}
|
||||
</SelectItem>
|
||||
<SelectItem value={ZTeamRole.enum.contributor}>
|
||||
{t("environments.settings.teams.contributor")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Delete Button for Member */}
|
||||
{watchMembers.length > 1 && (
|
||||
<TooltipRenderer tooltipContent={t("common.remove_from_team")}>
|
||||
<Button
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="destructive"
|
||||
className="shrink-0"
|
||||
disabled={
|
||||
!isOwnerOrManager &&
|
||||
(!isTeamAdminMember || member.userId === currentUserId)
|
||||
}
|
||||
onClick={() => handleRemoveMember(index)}>
|
||||
<XIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{watchMembers.map((member, index) => (
|
||||
<MemberRow
|
||||
key={`member-${member.userId}-${index}`}
|
||||
index={index}
|
||||
member={member}
|
||||
memberOpts={getMemberOptionsForIndex(index)}
|
||||
control={control}
|
||||
orgMembers={orgMembers}
|
||||
watchMembers={watchMembers}
|
||||
initialMemberIds={initialMemberIds}
|
||||
isOwnerOrManager={isOwnerOrManager}
|
||||
isTeamAdminMember={isTeamAdminMember}
|
||||
isTeamContributorMember={isTeamContributorMember}
|
||||
currentUserId={currentUserId}
|
||||
onMemberSelectionChange={handleMemberSelectionChange}
|
||||
onRemoveMember={handleRemoveMember}
|
||||
memberCount={watchMembers.length}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{error?.root?.message && (
|
||||
<FormError className="text-left">{error.root.message}</FormError>
|
||||
@@ -411,90 +323,19 @@ export const TeamSettingsModal = ({
|
||||
render={({ fieldState: { error } }) => (
|
||||
<FormItem className="flex-1">
|
||||
<div className="space-y-2">
|
||||
{watchProjects.map((project, index) => {
|
||||
const projectOpts = getProjectOptionsForIndex(index);
|
||||
return (
|
||||
<div key={`project-${project.projectId}-${index}`} className="flex gap-2.5">
|
||||
<FormField
|
||||
control={control}
|
||||
name={`projects.${index}.projectId`}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
// Disable project select for existing projects (can only remove or change permission)
|
||||
const isExistingProject =
|
||||
project.projectId && initialProjectIds.has(project.projectId);
|
||||
const isSelectDisabled = isExistingProject || !isOwnerOrManager;
|
||||
|
||||
return (
|
||||
<FormItem className="flex-1">
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={project.projectId}
|
||||
disabled={isSelectDisabled}>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t("environments.settings.teams.select_workspace")}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{projectOpts.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
id={`project-${index}-option`}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{error?.message && (
|
||||
<FormError className="text-left">{error.message}</FormError>
|
||||
)}
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={control}
|
||||
name={`projects.${index}.permission`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={project.permission}
|
||||
disabled={!isOwnerOrManager}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select project role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ZTeamPermission.enum.read}>
|
||||
{t("environments.settings.teams.read")}
|
||||
</SelectItem>
|
||||
<SelectItem value={ZTeamPermission.enum.readWrite}>
|
||||
{t("environments.settings.teams.read_write")}
|
||||
</SelectItem>
|
||||
<SelectItem value={ZTeamPermission.enum.manage}>
|
||||
{t("environments.settings.teams.manage")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
{watchProjects.map((project, index) => (
|
||||
<WorkspaceRow
|
||||
key={`workspace-${project.projectId}-${index}`}
|
||||
index={index}
|
||||
project={project}
|
||||
projectOpts={getProjectOptionsForIndex(index)}
|
||||
control={control}
|
||||
initialProjectIds={initialProjectIds}
|
||||
isOwnerOrManager={isOwnerOrManager}
|
||||
onRemoveProject={handleRemoveProject}
|
||||
projectCount={watchProjects.length}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{error?.root?.message && (
|
||||
<FormError className="text-left">{error.root.message}</FormError>
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
|
||||
import { Trash2Icon } from "lucide-react";
|
||||
import { Control } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ZTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
|
||||
import { TTeamSettingsFormSchema } from "@/modules/ee/teams/team-list/types/team";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { FormError, FormField, FormItem } from "@/modules/ui/components/form";
|
||||
import { InputCombobox } from "@/modules/ui/components/input-combo-box";
|
||||
|
||||
export interface WorkspaceRowProps {
|
||||
index: number;
|
||||
project: { projectId: string; permission: string };
|
||||
projectOpts: { value: string; label: string }[];
|
||||
control: Control<TTeamSettingsFormSchema>;
|
||||
initialProjectIds: Set<string>;
|
||||
isOwnerOrManager: boolean;
|
||||
onRemoveProject: (index: number) => void;
|
||||
projectCount: number;
|
||||
}
|
||||
|
||||
export function WorkspaceRow(props: Readonly<WorkspaceRowProps>) {
|
||||
const {
|
||||
index,
|
||||
project,
|
||||
projectOpts,
|
||||
control,
|
||||
initialProjectIds,
|
||||
isOwnerOrManager,
|
||||
onRemoveProject,
|
||||
projectCount,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex gap-2.5">
|
||||
<FormField
|
||||
control={control}
|
||||
name={`projects.${index}.projectId`}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
const isExistingProject = project.projectId && initialProjectIds.has(project.projectId);
|
||||
const isSelectDisabled = isExistingProject || !isOwnerOrManager;
|
||||
|
||||
return (
|
||||
<FormItem className="flex-1">
|
||||
<div className={isSelectDisabled ? "pointer-events-none opacity-50" : undefined}>
|
||||
<InputCombobox
|
||||
id={`project-select-${index}`}
|
||||
options={projectOpts}
|
||||
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>
|
||||
{error?.message && <FormError className="text-left">{error.message}</FormError>}
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={control}
|
||||
name={`projects.${index}.permission`}
|
||||
render={({ field }) => {
|
||||
const permissionOptions = [
|
||||
{
|
||||
value: ZTeamPermission.enum.read,
|
||||
label: t("environments.settings.teams.read"),
|
||||
},
|
||||
{
|
||||
value: ZTeamPermission.enum.readWrite,
|
||||
label: t("environments.settings.teams.read_write"),
|
||||
},
|
||||
{
|
||||
value: ZTeamPermission.enum.manage,
|
||||
label: t("environments.settings.teams.manage"),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<FormItem className="flex-1">
|
||||
<div className={isOwnerOrManager ? undefined : "pointer-events-none opacity-50"}>
|
||||
<InputCombobox
|
||||
id={`project-permission-select-${index}`}
|
||||
options={permissionOptions}
|
||||
value={field.value}
|
||||
onChangeValue={(val) => field.onChange(val)}
|
||||
showSearch={false}
|
||||
comboboxClasses="flex-1 min-w-0 w-full"
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{projectCount > 1 && (
|
||||
<Button
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="shrink-0"
|
||||
disabled={!isOwnerOrManager}
|
||||
onClick={() => onRemoveProject(index)}>
|
||||
<Trash2Icon className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
deleteTeam,
|
||||
getOtherTeams,
|
||||
getTeamDetails,
|
||||
getTeamIdsByUserIds,
|
||||
getTeams,
|
||||
getTeamsByOrganizationId,
|
||||
getUserTeams,
|
||||
@@ -24,6 +25,9 @@ vi.mock("@formbricks/database", () => ({
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
teamUser: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
membership: { findUnique: vi.fn(), count: vi.fn() },
|
||||
project: { count: vi.fn() },
|
||||
environment: { findMany: vi.fn() },
|
||||
@@ -81,6 +85,40 @@ describe("getTeamsByOrganizationId", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTeamIdsByUserIds", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
test("returns mapped team IDs by user ID", async () => {
|
||||
vi.mocked(prisma.teamUser.findMany).mockResolvedValueOnce([
|
||||
{ userId: "u1", teamId: "t1" },
|
||||
{ userId: "u1", teamId: "t2" },
|
||||
{ userId: "u2", teamId: "t1" },
|
||||
]);
|
||||
const result = await getTeamIdsByUserIds(["u1", "u2"], "org1");
|
||||
expect(result).toEqual({
|
||||
u1: ["t1", "t2"],
|
||||
u2: ["t1"],
|
||||
});
|
||||
});
|
||||
test("returns empty object when userIds is empty", async () => {
|
||||
const result = await getTeamIdsByUserIds([], "org1");
|
||||
expect(result).toEqual({});
|
||||
expect(prisma.teamUser.findMany).not.toHaveBeenCalled();
|
||||
});
|
||||
test("returns empty object when no team memberships exist", async () => {
|
||||
vi.mocked(prisma.teamUser.findMany).mockResolvedValueOnce([]);
|
||||
const result = await getTeamIdsByUserIds(["u1"], "org1");
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
test("throws DatabaseError on Prisma error", async () => {
|
||||
vi.mocked(prisma.teamUser.findMany).mockRejectedValueOnce(
|
||||
new Prisma.PrismaClientKnownRequestError("fail", { code: "P2002", clientVersion: "1.0.0" })
|
||||
);
|
||||
await expect(getTeamIdsByUserIds(["u1"], "org1")).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserTeams", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -95,6 +95,34 @@ export const getUserTeams = reactCache(
|
||||
}
|
||||
);
|
||||
|
||||
export const getTeamIdsByUserIds = reactCache(
|
||||
async (userIds: string[], organizationId: string): Promise<Record<string, string[]>> => {
|
||||
validateInputs([organizationId, ZId]);
|
||||
if (userIds.length === 0) return {};
|
||||
try {
|
||||
const teamUsers = await prisma.teamUser.findMany({
|
||||
where: {
|
||||
userId: { in: userIds },
|
||||
team: { organizationId },
|
||||
},
|
||||
select: { userId: true, teamId: true },
|
||||
});
|
||||
|
||||
const map: Record<string, string[]> = {};
|
||||
for (const tu of teamUsers) {
|
||||
if (!map[tu.userId]) map[tu.userId] = [];
|
||||
map[tu.userId].push(tu.teamId);
|
||||
}
|
||||
return map;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getOtherTeams = reactCache(
|
||||
async (userId: string, organizationId: string): Promise<TOtherTeam[]> => {
|
||||
validateInputs([userId, z.string()], [organizationId, ZId]);
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
"use client";
|
||||
|
||||
import { UsersRoundIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TMember } from "@formbricks/types/memberships";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { addMemberToTeamAction } from "@/modules/ee/teams/team-list/actions";
|
||||
import { TOrganizationTeam, ZTeamRole } from "@/modules/ee/teams/team-list/types/team";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { FormControl, FormField, FormItem, FormLabel, FormProvider } from "@/modules/ui/components/form";
|
||||
import { InputCombobox } from "@/modules/ui/components/input-combo-box";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/modules/ui/components/popover";
|
||||
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||
|
||||
type AssignToTeamFormValues = {
|
||||
teamId: string | null;
|
||||
role: "admin" | "contributor";
|
||||
};
|
||||
|
||||
interface AssignToTeamPopoverProps {
|
||||
member: TMember;
|
||||
assignableTeams: TOrganizationTeam[];
|
||||
memberTeamIdsMap: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export const AssignToTeamPopover = ({
|
||||
member,
|
||||
assignableTeams,
|
||||
memberTeamIdsMap,
|
||||
}: Readonly<AssignToTeamPopoverProps>) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [isAssigningToTeam, setIsAssigningToTeam] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const assignToTeamForm = useForm<AssignToTeamFormValues>({
|
||||
defaultValues: { teamId: null, role: "contributor" },
|
||||
});
|
||||
|
||||
const memberTeamIds = memberTeamIdsMap[member.userId] ?? [];
|
||||
const teamsToAssign = assignableTeams.filter((team) => !memberTeamIds.includes(team.id));
|
||||
const canAssignToTeam = teamsToAssign.length > 0;
|
||||
const isOwnerOrManager = member.role === "owner" || member.role === "manager";
|
||||
|
||||
const teamOptions = useMemo(
|
||||
() =>
|
||||
teamsToAssign
|
||||
.map((team) => ({ value: team.id, label: team.name }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" })),
|
||||
[teamsToAssign]
|
||||
);
|
||||
|
||||
const roleOptions = useMemo(
|
||||
() => [
|
||||
{ value: ZTeamRole.enum.admin, label: t("environments.settings.teams.team_admin") },
|
||||
{ value: ZTeamRole.enum.contributor, label: t("environments.settings.teams.contributor") },
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
||||
const handleAssignToTeam = async (values: AssignToTeamFormValues) => {
|
||||
if (!values.teamId) return;
|
||||
try {
|
||||
setIsAssigningToTeam(true);
|
||||
const result = await addMemberToTeamAction({
|
||||
teamId: values.teamId,
|
||||
userId: member.userId,
|
||||
role: isOwnerOrManager ? "admin" : values.role,
|
||||
});
|
||||
if (result?.data) {
|
||||
toast.success(t("environments.settings.teams.member_added_to_team"));
|
||||
setOpen(false);
|
||||
assignToTeamForm.reset({ teamId: null, role: "contributor" });
|
||||
router.refresh();
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(result);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(`${t("common.error")}: ${err.message}`);
|
||||
} finally {
|
||||
setIsAssigningToTeam(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
setOpen(newOpen);
|
||||
if (!newOpen) {
|
||||
assignToTeamForm.reset({ teamId: null, role: "contributor" });
|
||||
}
|
||||
};
|
||||
|
||||
const getTooltip = () => {
|
||||
if (canAssignToTeam) return t("common.add_to_team");
|
||||
if (assignableTeams.length === 0) {
|
||||
return t("environments.settings.teams.create_first_team_message");
|
||||
}
|
||||
return t("environments.settings.teams.member_in_all_teams");
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<TooltipRenderer tooltipContent={getTooltip()} shouldRender>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
id="assignToTeamButton"
|
||||
disabled={isAssigningToTeam || !canAssignToTeam}>
|
||||
<UsersRoundIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
</TooltipRenderer>
|
||||
<PopoverContent align="end" className="w-72">
|
||||
<FormProvider {...assignToTeamForm}>
|
||||
<form className="space-y-3" onSubmit={assignToTeamForm.handleSubmit(handleAssignToTeam)}>
|
||||
<FormField
|
||||
control={assignToTeamForm.control}
|
||||
name="teamId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("common.team_name")}</FormLabel>
|
||||
<FormControl>
|
||||
<InputCombobox
|
||||
id="assign-team-combobox"
|
||||
options={teamOptions}
|
||||
value={field.value}
|
||||
onChangeValue={(val) => field.onChange(typeof val === "string" ? val : null)}
|
||||
showSearch
|
||||
searchPlaceholder={t("common.search")}
|
||||
comboboxClasses="w-full text-sm"
|
||||
emptyDropdownText={t("environments.surveys.edit.no_option_found")}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={assignToTeamForm.control}
|
||||
name="role"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("common.team_role")}</FormLabel>
|
||||
{isOwnerOrManager ? (
|
||||
<div className="flex h-10 items-center rounded-md border border-slate-300 bg-slate-50 px-3 text-sm text-slate-600">
|
||||
{t("environments.settings.teams.team_admin")}
|
||||
</div>
|
||||
) : (
|
||||
<FormControl>
|
||||
<InputCombobox
|
||||
id="assign-role-combobox"
|
||||
options={roleOptions}
|
||||
value={field.value}
|
||||
onChangeValue={(val) => field.onChange(val as "admin" | "contributor")}
|
||||
showSearch={false}
|
||||
comboboxClasses="w-full text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
disabled={!assignToTeamForm.watch("teamId") || isAssigningToTeam}
|
||||
loading={isAssigningToTeam}>
|
||||
{t("common.add_to_team")}
|
||||
</Button>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,8 @@ import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getTeamIdsByUserIds } from "@/modules/ee/teams/team-list/lib/team";
|
||||
import { TOrganizationTeam } from "@/modules/ee/teams/team-list/types/team";
|
||||
import { MembersInfo } from "@/modules/organization/settings/teams/components/edit-memberships/members-info";
|
||||
import { getInvitesByOrganizationId } from "@/modules/organization/settings/teams/lib/invite";
|
||||
import { getMembershipByOrganizationId } from "@/modules/organization/settings/teams/lib/membership";
|
||||
@@ -12,6 +14,9 @@ interface EditMembershipsProps {
|
||||
role: TOrganizationRole;
|
||||
isAccessControlAllowed: boolean;
|
||||
isUserManagementDisabledFromUi: boolean;
|
||||
teams: TOrganizationTeam[];
|
||||
isOwnerOrManager: boolean;
|
||||
userAdminTeamIds?: string[];
|
||||
}
|
||||
|
||||
export const EditMemberships = async ({
|
||||
@@ -20,26 +25,35 @@ export const EditMemberships = async ({
|
||||
role,
|
||||
isAccessControlAllowed,
|
||||
isUserManagementDisabledFromUi,
|
||||
teams,
|
||||
isOwnerOrManager,
|
||||
userAdminTeamIds,
|
||||
}: EditMembershipsProps) => {
|
||||
const members = await getMembershipByOrganizationId(organization.id);
|
||||
const invites = await getInvitesByOrganizationId(organization.id);
|
||||
const t = await getTranslate();
|
||||
|
||||
const memberUserIds = (members ?? []).map((m) => m.userId);
|
||||
const memberTeamIdsMap =
|
||||
memberUserIds.length > 0 ? await getTeamIdsByUserIds(memberUserIds, organization.id) : {};
|
||||
|
||||
const assignableTeams = isOwnerOrManager
|
||||
? teams
|
||||
: teams.filter((team) => userAdminTeamIds?.includes(team.id));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="flex h-12 w-full max-w-full items-center gap-x-4 rounded-t-lg bg-slate-100 px-4 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="w-1/2 overflow-hidden">{t("common.full_name")}</div>
|
||||
<div className="w-1/2 overflow-hidden">{t("common.email")}</div>
|
||||
<div className="flex grid h-12 w-full max-w-full grid-cols-12 items-center gap-x-4 rounded-t-lg bg-slate-100 px-4 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-2 overflow-hidden">{t("common.full_name")}</div>
|
||||
<div className="col-span-3 overflow-hidden">{t("common.email")}</div>
|
||||
|
||||
{isAccessControlAllowed && (
|
||||
<div className="min-w-[100px] whitespace-nowrap">{t("common.role")}</div>
|
||||
)}
|
||||
{isAccessControlAllowed && <div className="col-span-2 whitespace-nowrap">{t("common.role")}</div>}
|
||||
|
||||
<div className="min-w-[80px] whitespace-nowrap">{t("common.status")}</div>
|
||||
<div className="col-span-2 whitespace-nowrap">{t("common.status")}</div>
|
||||
|
||||
{!isUserManagementDisabledFromUi && (
|
||||
<div className="min-w-[125px] whitespace-nowrap">{t("common.actions")}</div>
|
||||
<div className="col-span-3 whitespace-nowrap">{t("common.actions")}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -53,6 +67,8 @@ export const EditMemberships = async ({
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
isUserManagementDisabledFromUi={isUserManagementDisabledFromUi}
|
||||
assignableTeams={assignableTeams}
|
||||
memberTeamIdsMap={memberTeamIdsMap}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -8,12 +8,14 @@ import { useTranslation } from "react-i18next";
|
||||
import { TMember } from "@formbricks/types/memberships";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { TOrganizationTeam } from "@/modules/ee/teams/team-list/types/team";
|
||||
import {
|
||||
createInviteTokenAction,
|
||||
deleteInviteAction,
|
||||
deleteMembershipAction,
|
||||
resendInviteAction,
|
||||
} from "@/modules/organization/settings/teams/actions";
|
||||
import { AssignToTeamPopover } from "@/modules/organization/settings/teams/components/edit-memberships/assign-to-team-popover";
|
||||
import { ShareInviteModal } from "@/modules/organization/settings/teams/components/invite-member/share-invite-modal";
|
||||
import { TInvite } from "@/modules/organization/settings/teams/types/invites";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
@@ -25,15 +27,25 @@ interface MemberActionsProps {
|
||||
member?: TMember;
|
||||
invite?: TInvite;
|
||||
showDeleteButton?: boolean;
|
||||
assignableTeams: TOrganizationTeam[];
|
||||
memberTeamIdsMap: Record<string, string[]>;
|
||||
isAccessControlAllowed: boolean;
|
||||
}
|
||||
|
||||
export const MemberActions = ({ organization, member, invite, showDeleteButton }: MemberActionsProps) => {
|
||||
export const MemberActions = ({
|
||||
organization,
|
||||
member,
|
||||
invite,
|
||||
showDeleteButton,
|
||||
assignableTeams,
|
||||
memberTeamIdsMap,
|
||||
isAccessControlAllowed,
|
||||
}: MemberActionsProps) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [isDeleteMemberModalOpen, setDeleteMemberModalOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [showShareInviteModal, setShowShareInviteModal] = useState(false);
|
||||
|
||||
const [shareInviteToken, setShareInviteToken] = useState("");
|
||||
|
||||
const handleDeleteMember = async () => {
|
||||
@@ -123,6 +135,8 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
|
||||
}
|
||||
};
|
||||
|
||||
const showAssignToTeamButton = Boolean(member) && isAccessControlAllowed;
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<TooltipRenderer tooltipContent={t("common.delete")} shouldRender={!!showDeleteButton}>
|
||||
@@ -166,6 +180,14 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
|
||||
{showAssignToTeamButton && member && (
|
||||
<AssignToTeamPopover
|
||||
member={member}
|
||||
assignableTeams={assignableTeams}
|
||||
memberTeamIdsMap={memberTeamIdsMap}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DeleteDialog
|
||||
open={isDeleteMemberModalOpen}
|
||||
setOpen={setDeleteMemberModalOpen}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
import { getFormattedDateTimeString } from "@/lib/utils/datetime";
|
||||
import { EditMembershipRole } from "@/modules/ee/role-management/components/edit-membership-role";
|
||||
import { TOrganizationTeam } from "@/modules/ee/teams/team-list/types/team";
|
||||
import { MemberActions } from "@/modules/organization/settings/teams/components/edit-memberships/member-actions";
|
||||
import { isInviteExpired } from "@/modules/organization/settings/teams/lib/utils";
|
||||
import { TInvite } from "@/modules/organization/settings/teams/types/invites";
|
||||
@@ -21,6 +22,8 @@ interface MembersInfoProps {
|
||||
isAccessControlAllowed: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
isUserManagementDisabledFromUi: boolean;
|
||||
assignableTeams: TOrganizationTeam[];
|
||||
memberTeamIdsMap: Record<string, string[]>;
|
||||
}
|
||||
|
||||
// Type guard to check if member is an invitee
|
||||
@@ -37,6 +40,8 @@ export const MembersInfo = ({
|
||||
isAccessControlAllowed,
|
||||
isFormbricksCloud,
|
||||
isUserManagementDisabledFromUi,
|
||||
assignableTeams,
|
||||
memberTeamIdsMap,
|
||||
}: MembersInfoProps) => {
|
||||
const allMembers = [...members, ...invites];
|
||||
const { t } = useTranslation();
|
||||
@@ -96,17 +101,17 @@ export const MembersInfo = ({
|
||||
{allMembers.map((member) => (
|
||||
<div
|
||||
id="singleMemberInfo"
|
||||
className="flex w-full max-w-full items-center gap-x-4 text-left text-sm text-slate-900"
|
||||
className="flex grid w-full max-w-full grid-cols-12 items-center gap-x-4 text-left text-sm text-slate-900"
|
||||
key={member.email}>
|
||||
<div className="ph-no-capture w-1/2 overflow-hidden">
|
||||
<div className="ph-no-capture col-span-2 overflow-hidden">
|
||||
<p className="w-full truncate">{member.name}</p>
|
||||
</div>
|
||||
<div className="ph-no-capture w-1/2 overflow-hidden">
|
||||
<div className="ph-no-capture col-span-3 overflow-hidden">
|
||||
<p className="w-full truncate"> {member.email}</p>
|
||||
</div>
|
||||
|
||||
{isAccessControlAllowed && allMembers?.length > 0 && (
|
||||
<div className="ph-no-capture min-w-[100px]">
|
||||
<div className="ph-no-capture col-span-2">
|
||||
<EditMembershipRole
|
||||
currentUserRole={currentUserRole}
|
||||
memberRole={member.role}
|
||||
@@ -121,15 +126,20 @@ export const MembersInfo = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-[80px]">{getMembershipBadge(member)}</div>
|
||||
<div className="col-span-2 flex items-center">{getMembershipBadge(member)}</div>
|
||||
|
||||
{!isUserManagementDisabledFromUi && (
|
||||
<MemberActions
|
||||
organization={organization}
|
||||
member={!isInvitee(member) ? member : undefined}
|
||||
invite={isInvitee(member) ? member : undefined}
|
||||
showDeleteButton={showDeleteButton(member)}
|
||||
/>
|
||||
<div className="col-span-3">
|
||||
<MemberActions
|
||||
organization={organization}
|
||||
member={isInvitee(member) ? undefined : member}
|
||||
invite={isInvitee(member) ? member : undefined}
|
||||
showDeleteButton={showDeleteButton(member)}
|
||||
assignableTeams={assignableTeams}
|
||||
memberTeamIdsMap={memberTeamIdsMap}
|
||||
isAccessControlAllowed={isAccessControlAllowed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
||||
import { INVITE_DISABLED, IS_FORMBRICKS_CLOUD, IS_STORAGE_CONFIGURED } from "@/lib/constants";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getTeamsWhereUserIsAdmin } from "@/modules/ee/teams/lib/roles";
|
||||
@@ -56,6 +57,9 @@ export const MembersView = async ({
|
||||
teams = (await getTeamsByOrganizationId(organization.id)) ?? [];
|
||||
}
|
||||
|
||||
const { isOwner, isManager } = getAccessFlags(membershipRole);
|
||||
const isOwnerOrManager = isOwner || isManager;
|
||||
|
||||
return (
|
||||
<SettingsCard
|
||||
title={t("environments.settings.general.manage_members")}
|
||||
@@ -87,6 +91,9 @@ export const MembersView = async ({
|
||||
currentUserId={currentUserId}
|
||||
role={membershipRole}
|
||||
isUserManagementDisabledFromUi={isUserManagementDisabledFromUi}
|
||||
teams={teams}
|
||||
isOwnerOrManager={isOwnerOrManager}
|
||||
userAdminTeamIds={userAdminTeamIds}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user