Files
twenty/packages/twenty-front/src/pages/settings/roles/components/RoleAssignment.tsx
T
Weiko 02ced028e5 add role assignment page (#10115)
## Context
This PR introduces the "assignment" tab in the Role edit page, currently
allowing admin users to assign workspace members to specific roles.

Note: For now, a user can only have one role and a modal will warn you
if you try to re-assign a user to a new role.

## Test
<img width="648" alt="Screenshot 2025-02-10 at 17 59 21"
src="https://github.com/user-attachments/assets/dabd7a17-6aca-4d2b-95d8-46182f53e1e8"
/>
<img width="668" alt="Screenshot 2025-02-10 at 17 59 33"
src="https://github.com/user-attachments/assets/802aab7a-db67-4f83-9a44-35773df100f7"
/>
<img width="629" alt="Screenshot 2025-02-10 at 17 59 42"
src="https://github.com/user-attachments/assets/277db061-3f05-4ccd-8a83-7a96d6c1673e"
/>
2025-02-11 14:51:31 +01:00

228 lines
7.3 KiB
TypeScript

import { SettingsPath } from '@/types/SettingsPath';
import { TextInput } from '@/ui/input/components/TextInput';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { Table } from '@/ui/layout/table/components/Table';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { Button, H2Title, IconPlus, IconSearch, Section } from 'twenty-ui';
import { Role, WorkspaceMember } from '~/generated-metadata/graphql';
import {
GetRolesDocument,
useGetRolesQuery,
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';
import { RoleAssignmentTableRow } from './RoleAssignmentTableRow';
import { RoleWorkspaceMemberPickerDropdown } from './RoleWorkspaceMemberPickerDropdown';
const StyledBottomSection = styled(Section)<{ hasRows: boolean }>`
${({ hasRows, theme }) =>
hasRows
? `
border-top: 1px solid ${theme.border.color.light};
margin-top: ${theme.spacing(2)};
padding-top: ${theme.spacing(4)};
`
: `
margin-top: ${theme.spacing(8)};
`}
display: flex;
justify-content: flex-end;
`;
const StyledEmptyText = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.light};
display: flex;
justify-content: center;
margin-top: ${({ theme }) => theme.spacing(4)};
`;
const StyledSearchContainer = styled.div`
margin: ${({ theme }) => theme.spacing(2)} 0;
`;
const StyledSearchInput = styled(TextInput)`
input {
background: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium};
&:hover {
border: 1px solid ${({ theme }) => theme.border.color.medium};
}
}
`;
type RoleAssignmentProps = {
role: Pick<Role, 'id' | 'label' | 'canUpdateAllSettings'> & {
workspaceMembers: Array<WorkspaceMember>;
};
};
export const RoleAssignment = ({ role }: RoleAssignmentProps) => {
const navigateSettings = useNavigateSettings();
const [updateWorkspaceMemberRole] = useUpdateWorkspaceMemberRoleMutation({
refetchQueries: [GetRolesDocument],
});
const [modalMode, setModalMode] =
useState<RoleAssignmentConfirmationModalMode | null>(null);
const [selectedWorkspaceMember, setSelectedWorkspaceMember] =
useState<RoleAssignmentConfirmationModalSelectedWorkspaceMember | null>(
null,
);
const { data: rolesData } = useGetRolesQuery();
const { closeDropdown } = useDropdown('role-member-select');
const [searchFilter, setSearchFilter] = useState('');
const workspaceMemberRoleMap = new Map<
string,
{ id: string; label: string }
>();
rolesData?.getRoles?.forEach((role) => {
role.workspaceMembers.forEach((member) => {
workspaceMemberRoleMap.set(member.id, { id: role.id, label: role.label });
});
});
const filteredWorkspaceMembers = !searchFilter
? role.workspaceMembers
: role.workspaceMembers.filter((member) => {
const searchTerm = searchFilter.toLowerCase();
const firstName = member.name.firstName?.toLowerCase() || '';
const lastName = member.name.lastName?.toLowerCase() || '';
const email = member.userEmail?.toLowerCase() || '';
return (
firstName.includes(searchTerm) ||
lastName.includes(searchTerm) ||
email.includes(searchTerm)
);
});
const handleModalClose = () => {
setModalMode(null);
setSelectedWorkspaceMember(null);
};
const handleSelectWorkspaceMember = (workspaceMember: WorkspaceMember) => {
const existingRole = workspaceMemberRoleMap.get(workspaceMember.id);
setSelectedWorkspaceMember({
id: workspaceMember.id,
name: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
role: existingRole,
});
setModalMode('assign');
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;
await updateWorkspaceMemberRole({
variables: {
workspaceMemberId: selectedWorkspaceMember.id,
roleId: modalMode === 'assign' ? role.id : null,
},
});
handleModalClose();
};
const handleRoleClick = (roleId: string) => {
navigateSettings(SettingsPath.RoleDetail, { roleId });
handleModalClose();
};
const handleSearchChange = (text: string) => {
setSearchFilter(text);
};
return (
<>
<Section>
<H2Title
title={t`Assigned members`}
description={t`This role is assigned to these workspace members.`}
/>
<StyledSearchContainer>
<StyledSearchInput
value={searchFilter}
onChange={handleSearchChange}
placeholder={t`Search a member`}
fullWidth
LeftIcon={IconSearch}
sizeVariant="lg"
/>
</StyledSearchContainer>
<Table>
<RoleAssignmentTableHeader />
{filteredWorkspaceMembers.map((workspaceMember) => (
<RoleAssignmentTableRow
key={workspaceMember.id}
workspaceMember={workspaceMember}
onRemove={() => handleRemoveClick(workspaceMember)}
/>
))}
{filteredWorkspaceMembers.length === 0 && (
<StyledEmptyText>
{searchFilter
? t`No members matching your search`
: t`No members assigned to this role yet`}
</StyledEmptyText>
)}
</Table>
</Section>
<StyledBottomSection hasRows={filteredWorkspaceMembers.length > 0}>
<Dropdown
dropdownId="role-member-select"
dropdownHotkeyScope={{ scope: 'roleAssignment' }}
clickableComponent={
<Button
Icon={IconPlus}
title={t`Assign to member`}
variant="secondary"
size="small"
/>
}
dropdownComponents={
<RoleWorkspaceMemberPickerDropdown
excludedWorkspaceMemberIds={role.workspaceMembers.map(
(workspaceMember) => workspaceMember.id,
)}
onSelect={handleSelectWorkspaceMember}
/>
}
/>
</StyledBottomSection>
{modalMode && selectedWorkspaceMember && (
<RoleAssignmentConfirmationModal
mode={modalMode}
selectedWorkspaceMember={selectedWorkspaceMember}
isOpen={true}
onClose={handleModalClose}
onConfirm={handleConfirm}
onRoleClick={handleRoleClick}
/>
)}
</>
);
};