[permissions] Update updateRole logic + disallow self role-assignment (#10476)
In this PR - updateWorkspaceMemberRole api was changed to stop allowing null as a valid value for roleId. it is not possible anymore to just unassign a role from a user. instead it is only possible to assign a different role to a user, which will unassign them from their previous role. For this reason in the FE the bins icons next to the workspaceMember on a role page were removed - updateWorkspaceMemberRole will throw if a user attempts to update their own role - tests tests tests!
This commit is contained in:
@ -1085,7 +1085,7 @@ export type MutationUpdateWorkspaceFeatureFlagArgs = {
|
||||
|
||||
|
||||
export type MutationUpdateWorkspaceMemberRoleArgs = {
|
||||
roleId?: InputMaybe<Scalars['String']>;
|
||||
roleId: Scalars['String'];
|
||||
workspaceMemberId: Scalars['String'];
|
||||
};
|
||||
|
||||
@ -2368,7 +2368,7 @@ export type RoleFragmentFragment = { __typename?: 'Role', id: string, label: str
|
||||
|
||||
export type UpdateWorkspaceMemberRoleMutationVariables = Exact<{
|
||||
workspaceMemberId: Scalars['String'];
|
||||
roleId?: InputMaybe<Scalars['String']>;
|
||||
roleId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
@ -4229,7 +4229,7 @@ export type UpdateLabPublicFeatureFlagMutationHookResult = ReturnType<typeof use
|
||||
export type UpdateLabPublicFeatureFlagMutationResult = Apollo.MutationResult<UpdateLabPublicFeatureFlagMutation>;
|
||||
export type UpdateLabPublicFeatureFlagMutationOptions = Apollo.BaseMutationOptions<UpdateLabPublicFeatureFlagMutation, UpdateLabPublicFeatureFlagMutationVariables>;
|
||||
export const UpdateWorkspaceMemberRoleDocument = gql`
|
||||
mutation UpdateWorkspaceMemberRole($workspaceMemberId: String!, $roleId: String) {
|
||||
mutation UpdateWorkspaceMemberRole($workspaceMemberId: String!, $roleId: String!) {
|
||||
updateWorkspaceMemberRole(
|
||||
workspaceMemberId: $workspaceMemberId
|
||||
roleId: $roleId
|
||||
|
||||
@ -7,7 +7,7 @@ export const UPDATE_WORKSPACE_MEMBER_ROLE = gql`
|
||||
${ROLE_FRAGMENT}
|
||||
mutation UpdateWorkspaceMemberRole(
|
||||
$workspaceMemberId: String!
|
||||
$roleId: String
|
||||
$roleId: String!
|
||||
) {
|
||||
updateWorkspaceMemberRole(
|
||||
workspaceMemberId: $workspaceMemberId
|
||||
|
||||
@ -24,7 +24,6 @@ import {
|
||||
useUpdateWorkspaceMemberRoleMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { RoleAssignmentConfirmationModalMode } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalMode';
|
||||
import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember';
|
||||
import { RoleAssignmentConfirmationModal } from './RoleAssignmentConfirmationModal';
|
||||
import { RoleAssignmentTableHeader } from './RoleAssignmentTableHeader';
|
||||
@ -73,8 +72,8 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
|
||||
refetchQueries: [GetRolesDocument],
|
||||
});
|
||||
|
||||
const [modalMode, setModalMode] =
|
||||
useState<RoleAssignmentConfirmationModalMode | null>(null);
|
||||
const [confirmationModalIsOpen, setConfirmationModalIsOpen] =
|
||||
useState<boolean>(false);
|
||||
const [selectedWorkspaceMember, setSelectedWorkspaceMember] =
|
||||
useState<RoleAssignmentConfirmationModalSelectedWorkspaceMember | null>(
|
||||
null,
|
||||
@ -110,7 +109,7 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
|
||||
});
|
||||
|
||||
const handleModalClose = () => {
|
||||
setModalMode(null);
|
||||
setConfirmationModalIsOpen(false);
|
||||
setSelectedWorkspaceMember(null);
|
||||
};
|
||||
|
||||
@ -122,26 +121,17 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
|
||||
name: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
|
||||
role: existingRole,
|
||||
});
|
||||
setModalMode('assign');
|
||||
setConfirmationModalIsOpen(true);
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const handleRemoveClick = (workspaceMember: WorkspaceMember) => {
|
||||
setSelectedWorkspaceMember({
|
||||
id: workspaceMember.id,
|
||||
name: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
|
||||
role: workspaceMemberRoleMap.get(workspaceMember.id),
|
||||
});
|
||||
setModalMode('remove');
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!selectedWorkspaceMember || !modalMode) return;
|
||||
if (!selectedWorkspaceMember || !confirmationModalIsOpen) return;
|
||||
|
||||
await updateWorkspaceMemberRole({
|
||||
variables: {
|
||||
workspaceMemberId: selectedWorkspaceMember.id,
|
||||
roleId: modalMode === 'assign' ? role.id : null,
|
||||
roleId: role.id,
|
||||
},
|
||||
});
|
||||
|
||||
@ -183,7 +173,6 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
|
||||
<RoleAssignmentTableRow
|
||||
key={workspaceMember.id}
|
||||
workspaceMember={workspaceMember}
|
||||
onRemove={() => handleRemoveClick(workspaceMember)}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
@ -222,9 +211,8 @@ export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
|
||||
/>
|
||||
</StyledBottomSection>
|
||||
|
||||
{modalMode && selectedWorkspaceMember && (
|
||||
{confirmationModalIsOpen && selectedWorkspaceMember && (
|
||||
<RoleAssignmentConfirmationModal
|
||||
mode={modalMode}
|
||||
selectedWorkspaceMember={selectedWorkspaceMember}
|
||||
isOpen={true}
|
||||
onClose={handleModalClose}
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { RoleAssignmentConfirmationModalSubtitle } from '~/pages/settings/roles/components/RoleAssignmentConfirmationModalSubtitle';
|
||||
import { RoleAssignmentConfirmationModalMode } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalMode';
|
||||
import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember';
|
||||
|
||||
type RoleAssignmentConfirmationModalProps = {
|
||||
mode: RoleAssignmentConfirmationModalMode;
|
||||
selectedWorkspaceMember: RoleAssignmentConfirmationModalSelectedWorkspaceMember;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
@ -14,21 +12,15 @@ type RoleAssignmentConfirmationModalProps = {
|
||||
};
|
||||
|
||||
export const RoleAssignmentConfirmationModal = ({
|
||||
mode,
|
||||
selectedWorkspaceMember,
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
onRoleClick,
|
||||
}: RoleAssignmentConfirmationModalProps) => {
|
||||
const isAssignMode = mode === 'assign';
|
||||
const hasExistingRole = !!selectedWorkspaceMember.role;
|
||||
|
||||
const workspaceMemberName = selectedWorkspaceMember.name;
|
||||
|
||||
const title = isAssignMode
|
||||
? t`Assign ${workspaceMemberName}?`
|
||||
: t`Remove ${workspaceMemberName}?`;
|
||||
const title = t`Assign ${workspaceMemberName}?`;
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
@ -37,14 +29,13 @@ export const RoleAssignmentConfirmationModal = ({
|
||||
title={title}
|
||||
subtitle={
|
||||
<RoleAssignmentConfirmationModalSubtitle
|
||||
mode={mode}
|
||||
selectedWorkspaceMember={selectedWorkspaceMember}
|
||||
onRoleClick={onRoleClick}
|
||||
/>
|
||||
}
|
||||
onConfirmClick={onConfirm}
|
||||
deleteButtonText={isAssignMode ? t`Confirm` : t`Remove`}
|
||||
confirmButtonAccent={isAssignMode && !hasExistingRole ? 'blue' : 'danger'}
|
||||
deleteButtonText={t`Confirm`}
|
||||
confirmButtonAccent="blue"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,7 +2,6 @@ import { SettingsCard } from '@/settings/components/SettingsCard';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconUser } from 'twenty-ui';
|
||||
import { RoleAssignmentConfirmationModalMode } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalMode';
|
||||
import { RoleAssignmentConfirmationModalSelectedWorkspaceMember } from '~/pages/settings/roles/types/RoleAssignmentConfirmationModalSelectedWorkspaceMember';
|
||||
|
||||
const StyledSettingsCardContainer = styled.div`
|
||||
@ -10,40 +9,29 @@ const StyledSettingsCardContainer = styled.div`
|
||||
`;
|
||||
|
||||
type RoleAssignmentConfirmationModalSubtitleProps = {
|
||||
mode: RoleAssignmentConfirmationModalMode;
|
||||
selectedWorkspaceMember: RoleAssignmentConfirmationModalSelectedWorkspaceMember;
|
||||
onRoleClick: (roleId: string) => void;
|
||||
};
|
||||
|
||||
export const RoleAssignmentConfirmationModalSubtitle = ({
|
||||
mode,
|
||||
selectedWorkspaceMember,
|
||||
onRoleClick,
|
||||
}: RoleAssignmentConfirmationModalSubtitleProps) => {
|
||||
const isAssignMode = mode === 'assign';
|
||||
const hasExistingRole = !!selectedWorkspaceMember.role;
|
||||
|
||||
const workspaceMemberName = selectedWorkspaceMember.name;
|
||||
|
||||
if (isAssignMode && hasExistingRole) {
|
||||
return (
|
||||
<>
|
||||
{t`${workspaceMemberName} will be unassigned from the following role:`}
|
||||
<StyledSettingsCardContainer>
|
||||
<SettingsCard
|
||||
title={selectedWorkspaceMember.role?.label || ''}
|
||||
Icon={<IconUser />}
|
||||
onClick={() =>
|
||||
selectedWorkspaceMember.role &&
|
||||
onRoleClick(selectedWorkspaceMember.role.id)
|
||||
}
|
||||
/>
|
||||
</StyledSettingsCardContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return isAssignMode
|
||||
? t`Are you sure you want to assign this role?`
|
||||
: t`This member will be unassigned from this role.`;
|
||||
return (
|
||||
<>
|
||||
{t`${workspaceMemberName} will be unassigned from the following role:`}
|
||||
<StyledSettingsCardContainer>
|
||||
<SettingsCard
|
||||
title={selectedWorkspaceMember.role?.label || ''}
|
||||
Icon={<IconUser />}
|
||||
onClick={() =>
|
||||
selectedWorkspaceMember.role &&
|
||||
onRoleClick(selectedWorkspaceMember.role.id)
|
||||
}
|
||||
/>
|
||||
</StyledSettingsCardContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,13 +2,7 @@ import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
Avatar,
|
||||
IconButton,
|
||||
IconTrash,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui';
|
||||
import { Avatar, OverflowingTextWithTooltip } from 'twenty-ui';
|
||||
import { WorkspaceMember } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledTable = styled(Table)`
|
||||
@ -21,27 +15,13 @@ const StyledIconWrapper = styled.div`
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
type RoleAssignmentTableRowProps = {
|
||||
workspaceMember: WorkspaceMember;
|
||||
onRemove: (workspaceMemberId: string) => void;
|
||||
};
|
||||
|
||||
export const RoleAssignmentTableRow = ({
|
||||
workspaceMember,
|
||||
onRemove,
|
||||
}: RoleAssignmentTableRowProps) => {
|
||||
const handleRemoveClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
onRemove(workspaceMember.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledTable>
|
||||
<TableRow gridAutoColumns="150px 1fr 1fr">
|
||||
@ -62,17 +42,6 @@ export const RoleAssignmentTableRow = ({
|
||||
<TableCell>
|
||||
<OverflowingTextWithTooltip text={workspaceMember.userEmail} />
|
||||
</TableCell>
|
||||
<TableCell align={'right'}>
|
||||
<StyledButtonContainer>
|
||||
<IconButton
|
||||
onClick={handleRemoveClick}
|
||||
variant="tertiary"
|
||||
size="medium"
|
||||
Icon={IconTrash}
|
||||
aria-label={t`Remove`}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</StyledTable>
|
||||
);
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export type RoleAssignmentConfirmationModalMode = 'assign' | 'remove';
|
||||
Reference in New Issue
Block a user