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. <img width="1281" height="813" alt="Permissions (If easier V1)" src="https://github.com/user-attachments/assets/7b272f5a-40ef-43ad-83d8-f9e588b1cd6e" /> Therefore, I added a Select component for now and would love some clarity on what's ideal. <img width="554" height="651" alt="image" src="https://github.com/user-attachments/assets/91575208-66b1-4ed1-86cd-f3aa528ad0dc" /> 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.
This commit is contained in:
+162
@@ -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<RoleWithPartialMembers | null>(
|
||||
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 (
|
||||
<StyledNoRoleContainer>{t`No role assigned to this member`}</StyledNoRoleContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const oldRoleLabel = primaryRole.label;
|
||||
const newRoleLabel = pendingRole?.label || '';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Role`}
|
||||
description={t`Customize what this user can view and perform`}
|
||||
/>
|
||||
<StyledRoleContainer>
|
||||
<StyledRoleSelector>
|
||||
<Select
|
||||
dropdownId="member-role-select"
|
||||
options={rolesOptions}
|
||||
value={primaryRole.id}
|
||||
onChange={handleRoleChangeRequest}
|
||||
withSearchInput
|
||||
fullWidth
|
||||
/>
|
||||
</StyledRoleSelector>
|
||||
<Button
|
||||
Icon={IconArrowUpRight}
|
||||
title={t`Open in Roles`}
|
||||
variant="secondary"
|
||||
onClick={handleOpenRole}
|
||||
/>
|
||||
</StyledRoleContainer>
|
||||
<SettingsRolePermissions roleId={primaryRole.id} isEditable={false} />
|
||||
</Section>
|
||||
|
||||
{pendingRole && (
|
||||
<ConfirmationModal
|
||||
modalId={CONFIRM_ROLE_CHANGE_MODAL_ID}
|
||||
title={t`Confirm role update`}
|
||||
subtitle={t`Are you sure you want to update the role of this user from "${oldRoleLabel}" to "${newRoleLabel}"?`}
|
||||
onConfirmClick={handleConfirmRoleChange}
|
||||
confirmButtonText={t`Update role`}
|
||||
confirmButtonAccent="blue"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { settingsAllRolesSelector } from '@/settings/roles/states/settingsAllRolesSelector';
|
||||
import { settingsRolesIsLoadingState } from '@/settings/roles/states/settingsRolesIsLoadingState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
export const useWorkspaceMemberRoles = (workspaceMemberId: string) => {
|
||||
const settingsAllRoles = useRecoilValue(settingsAllRolesSelector);
|
||||
const settingsRolesIsLoading = useRecoilValue(settingsRolesIsLoadingState);
|
||||
|
||||
const roles = workspaceMemberId
|
||||
? settingsAllRoles.filter((role) =>
|
||||
role.workspaceMembers.some((member) => member.id === workspaceMemberId),
|
||||
)
|
||||
: [];
|
||||
|
||||
return {
|
||||
roles,
|
||||
allRoles: settingsAllRoles,
|
||||
loading: settingsRolesIsLoading,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user