From eecc7aaed3b11891166cbf2e093f7ef206c76811 Mon Sep 17 00:00:00 2001 From: "Abdullah." <125115953+mabdullahabaid@users.noreply.github.com> Date: Tue, 2 Dec 2025 19:21:17 +0500 Subject: [PATCH] Workspace member permission tab. (#16233) Introduce a read-only view of permissions similar to agent roles tab. The role selector in the following image feels as if it would lead to a new page, which is not what we want. Permissions (If easier V1) Therefore, I added a Select component for now and would love some clarity on what's ideal. image Also, the "Add Rule" button changes to enabled when we switch the role from `Admin` to `Member` and clicking it redirects to `/settings/roles/:role-id/add-object-permission`. Do we want to disable the button completely? One final thing: SettingsAgentRoleTab already re-uses SettingsRolePermissions. I created MemberPermissionsTab to do the same. I don't think we need an abstracted shared component here since both tabs rely on the same shared child anyway. However, if we need a refactor, I can use some direction on how the code should look. Creating this PR as draft since I think there might be a couple suggested changes on this. --- .../components/MemberPermissionsTab.tsx | 162 ++++++++++++++++++ .../members/hooks/useWorkspaceMemberRoles.ts | 20 +++ .../members/SettingsWorkspaceMember.tsx | 141 +++++++++------ 3 files changed, 269 insertions(+), 54 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/members/components/MemberPermissionsTab.tsx create mode 100644 packages/twenty-front/src/modules/settings/members/hooks/useWorkspaceMemberRoles.ts diff --git a/packages/twenty-front/src/modules/settings/members/components/MemberPermissionsTab.tsx b/packages/twenty-front/src/modules/settings/members/components/MemberPermissionsTab.tsx new file mode 100644 index 0000000000..4f332ca790 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/members/components/MemberPermissionsTab.tsx @@ -0,0 +1,162 @@ +import { SettingsRolePermissions } from '@/settings/roles/role-permissions/components/SettingsRolePermissions'; +import { type RoleWithPartialMembers } from '@/settings/roles/types/RoleWithPartialMembers'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { Select } from '@/ui/input/components/Select'; +import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; +import { useModal } from '@/ui/layout/modal/hooks/useModal'; +import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; +import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; +import { useState } from 'react'; +import { SettingsPath } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { + H2Title, + IconArrowUpRight, + IconUser, + useIcons, +} from 'twenty-ui/display'; +import { Button } from 'twenty-ui/input'; +import { Section } from 'twenty-ui/layout'; +import { useUpdateWorkspaceMemberRoleMutation } from '~/generated-metadata/graphql'; +import { useNavigateSettings } from '~/hooks/useNavigateSettings'; + +const CONFIRM_ROLE_CHANGE_MODAL_ID = 'confirm-role-change-modal'; + +const StyledNoRoleContainer = styled.div` + align-items: center; + color: ${({ theme }) => theme.font.color.tertiary}; + display: flex; + justify-content: center; + padding: ${({ theme }) => theme.spacing(8)}; +`; + +const StyledRoleContainer = styled.div` + align-items: flex-end; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + margin-bottom: ${({ theme }) => theme.spacing(8)}; +`; + +const StyledRoleSelector = styled.div` + flex: 1; +`; + +type MemberPermissionsTabProps = { + member: WorkspaceMember; + roles: RoleWithPartialMembers[]; + allRoles: RoleWithPartialMembers[]; +}; + +export const MemberPermissionsTab = ({ + member, + roles, + allRoles, +}: MemberPermissionsTabProps) => { + const primaryRole = roles?.[0]; + const { getIcon } = useIcons(); + const navigateSettings = useNavigateSettings(); + const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); + const { openModal } = useModal(); + const [pendingRole, setPendingRole] = useState( + null, + ); + + const [updateWorkspaceMemberRoleMutation] = + useUpdateWorkspaceMemberRoleMutation(); + + const rolesOptions = + allRoles + ?.filter((role) => role.canBeAssignedToUsers) + .map((role) => ({ + label: role.label, + value: role.id, + Icon: getIcon(role.icon) ?? IconUser, + })) || []; + + const handleRoleChangeRequest = (newRoleId: string) => { + const newRole = allRoles.find((role) => role.id === newRoleId); + if (!newRole || newRoleId === primaryRole?.id) return; + + setPendingRole(newRole); + openModal(CONFIRM_ROLE_CHANGE_MODAL_ID); + }; + + const handleConfirmRoleChange = async () => { + if (!member?.id || !pendingRole) return; + + try { + await updateWorkspaceMemberRoleMutation({ + variables: { + workspaceMemberId: member.id, + roleId: pendingRole.id, + }, + refetchQueries: ['GetRoles'], + }); + enqueueSuccessSnackBar({ message: t`Role updated successfully` }); + } catch (error) { + enqueueErrorSnackBar({ + message: + error instanceof Error ? error.message : t`Failed to update role`, + }); + } finally { + setPendingRole(null); + } + }; + + const handleOpenRole = () => { + if (isDefined(primaryRole)) { + navigateSettings(SettingsPath.RoleDetail, { roleId: primaryRole.id }); + } + }; + + if (!primaryRole) { + return ( + {t`No role assigned to this member`} + ); + } + + const oldRoleLabel = primaryRole.label; + const newRoleLabel = pendingRole?.label || ''; + + return ( + <> +
+ + + +