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"
/>
This commit is contained in:
Weiko
2025-02-11 14:51:31 +01:00
committed by GitHub
parent 179d3ae2a4
commit 02ced028e5
26 changed files with 813 additions and 70 deletions

View File

@ -2,7 +2,13 @@ import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { H3Title, IconLockOpen, IconUser, IconUserPlus } from 'twenty-ui';
import {
H3Title,
IconLockOpen,
IconSettings,
IconUser,
IconUserPlus,
} from 'twenty-ui';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsPath } from '@/types/SettingsPath';
@ -12,6 +18,7 @@ import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { useGetRolesQuery } from '~/generated/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { RolePermissions } from '~/pages/settings/roles/components/RolePermissions';
import { RoleSettings } from '~/pages/settings/roles/components/RoleSettings';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { RoleAssignment } from './components/RoleAssignment';
@ -36,13 +43,16 @@ export const SETTINGS_ROLE_DETAIL_TABS = {
TABS_IDS: {
ASSIGNMENT: 'assignment',
PERMISSIONS: 'permissions',
SETTINGS: 'settings',
},
} as const;
export const SettingsRoleEdit = () => {
const { roleId = '' } = useParams();
const { data: rolesData, loading: rolesLoading } = useGetRolesQuery();
const navigateSettings = useNavigateSettings();
const { data: rolesData, loading: rolesLoading } = useGetRolesQuery({
fetchPolicy: 'network-only',
});
const role = rolesData?.getRoles.find((r) => r.id === roleId);
@ -71,6 +81,12 @@ export const SettingsRoleEdit = () => {
Icon: IconLockOpen,
hide: false,
},
{
id: SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.SETTINGS,
title: t`Settings`,
Icon: IconSettings,
hide: false,
},
];
const renderActiveTabContent = () => {
@ -79,6 +95,8 @@ export const SettingsRoleEdit = () => {
return <RoleAssignment role={role} />;
case SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.PERMISSIONS:
return <RolePermissions role={role} />;
case SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.SETTINGS:
return <RoleSettings role={role} />;
default:
return null;
}

View File

@ -55,8 +55,7 @@ const StyledAvatarGroup = styled.div`
margin-right: ${({ theme }) => theme.spacing(1)};
> * {
border: 2px solid ${({ theme }) => theme.background.primary};
margin-left: -8px;
margin-left: -5px;
&:first-of-type {
margin-left: 0;
@ -84,6 +83,11 @@ const StyledAvatarContainer = styled.div`
border: 0px;
`;
const StyledAssignedText = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
`;
export const SettingsRoles = () => {
const { t } = useLingui();
const isPermissionsEnabled = useIsFeatureEnabled(
@ -91,8 +95,9 @@ export const SettingsRoles = () => {
);
const theme = useTheme();
const navigateSettings = useNavigateSettings();
const { data: rolesData, loading: isRolesLoading } = useGetRolesQuery();
const { data: rolesData, loading: rolesLoading } = useGetRolesQuery({
fetchPolicy: 'network-only',
});
if (!isPermissionsEnabled) {
return null;
@ -131,7 +136,7 @@ export const SettingsRoles = () => {
<TableHeader align={'right'}></TableHeader>
</TableRow>
</StyledTableHeaderRow>
{!isRolesLoading &&
{!rolesLoading &&
rolesData?.getRoles.map((role) => (
<StyledTableRow
key={role.id}
@ -164,7 +169,7 @@ export const SettingsRoles = () => {
workspaceMember.name.firstName ?? ''
}
type="rounded"
size="sm"
size="md"
/>
</StyledAvatarContainer>
<AppTooltip
@ -178,7 +183,9 @@ export const SettingsRoles = () => {
</>
))}
</StyledAvatarGroup>
{role.workspaceMembers.length}
<StyledAssignedText>
{role.workspaceMembers.length}
</StyledAssignedText>
</StyledAssignedCell>
</TableCell>
<TableCell align={'right'}>

View File

@ -1,19 +1,227 @@
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 { H2Title, Section } from 'twenty-ui';
import { Role } from '~/generated-metadata/graphql';
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'>;
role: Pick<Role, 'id' | 'label' | 'canUpdateAllSettings'> & {
workspaceMembers: Array<WorkspaceMember>;
};
};
// eslint-disable-next-line unused-imports/no-unused-vars
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 member.`}
/>
</Section>
<>
<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}
/>
)}
</>
);
};

View File

@ -0,0 +1,50 @@
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;
onConfirm: () => void;
onRoleClick: (roleId: string) => void;
};
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}?`;
return (
<ConfirmationModal
isOpen={isOpen}
setIsOpen={onClose}
title={title}
subtitle={
<RoleAssignmentConfirmationModalSubtitle
mode={mode}
selectedWorkspaceMember={selectedWorkspaceMember}
onRoleClick={onRoleClick}
/>
}
onConfirmClick={onConfirm}
deleteButtonText={isAssignMode ? t`Confirm` : t`Remove`}
confirmButtonAccent={isAssignMode && !hasExistingRole ? 'blue' : 'danger'}
/>
);
};

View File

@ -0,0 +1,49 @@
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`
margin-top: ${({ theme }) => theme.spacing(2)};
`;
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.`;
};

View File

@ -0,0 +1,25 @@
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
const StyledTableHeaderRow = styled(Table)`
margin-bottom: ${({ theme }) => theme.spacing(1.5)};
`;
type RoleAssignmentTableHeaderProps = {
className?: string;
};
export const RoleAssignmentTableHeader = ({
className,
}: RoleAssignmentTableHeaderProps) => (
<StyledTableHeaderRow className={className}>
<TableRow gridAutoColumns="150px 1fr 1fr">
<TableHeader>{t`Name`}</TableHeader>
<TableHeader>{t`Email`}</TableHeader>
<TableHeader align={'right'} aria-label={t`Actions`}></TableHeader>
</TableRow>
</StyledTableHeaderRow>
);

View File

@ -0,0 +1,79 @@
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 { WorkspaceMember } from '~/generated-metadata/graphql';
const StyledTable = styled(Table)`
margin-top: ${({ theme }) => theme.spacing(0.5)};
`;
const StyledIconWrapper = styled.div`
display: flex;
align-items: center;
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">
<TableCell>
<StyledIconWrapper>
<Avatar
avatarUrl={workspaceMember.avatarUrl}
placeholderColorSeed={workspaceMember.id}
placeholder={workspaceMember.name.firstName ?? ''}
type="rounded"
size="md"
/>
</StyledIconWrapper>
<OverflowingTextWithTooltip
text={`${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`}
/>
</TableCell>
<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>
);
};

View File

@ -0,0 +1,46 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { IconPicker } from '@/ui/input/components/IconPicker';
import { TextArea } from '@/ui/input/components/TextArea';
import { TextInput } from '@/ui/input/components/TextInput';
import { Role } from '~/generated-metadata/graphql';
const StyledInputsContainer = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
margin-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledInputContainer = styled.div`
display: flex;
flex-direction: column;
`;
type RoleSettingsProps = {
role: Pick<Role, 'id' | 'label' | 'description'>;
};
export const RoleSettings = ({ role }: RoleSettingsProps) => {
return (
<>
<StyledInputsContainer>
<StyledInputContainer>
<IconPicker
disabled={true}
selectedIconKey={'IconUser'}
onChange={() => {}}
/>
</StyledInputContainer>
<TextInput value={role.label} disabled fullWidth />
</StyledInputsContainer>
<TextArea
minRows={4}
placeholder={t`Write a description`}
value={role.description || ''}
disabled
/>
</>
);
};

View File

@ -0,0 +1,71 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import styled from '@emotion/styled';
import { ChangeEvent, useState } from 'react';
import { WorkspaceMember } from '~/generated-metadata/graphql';
import { RoleWorkspaceMemberPickerDropdownContent } from './RoleWorkspaceMemberPickerDropdownContent';
const StyledWorkspaceMemberSelectContainer = styled.div`
max-height: ${({ theme }) => theme.spacing(50)};
overflow-y: auto;
`;
type RoleWorkspaceMemberPickerDropdownProps = {
excludedWorkspaceMemberIds: string[];
onSelect: (workspaceMember: WorkspaceMember) => void;
};
export const RoleWorkspaceMemberPickerDropdown = ({
excludedWorkspaceMemberIds,
onSelect,
}: RoleWorkspaceMemberPickerDropdownProps) => {
const [searchFilter, setSearchFilter] = useState('');
const { records: workspaceMembers, loading } = useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
filter: searchFilter
? {
or: [
{
name: { firstName: { ilike: `%${searchFilter}%` } },
},
{
name: { lastName: { ilike: `%${searchFilter}%` } },
},
{
userEmail: { ilike: `%${searchFilter}%` },
},
],
}
: undefined,
});
const filteredWorkspaceMembers = (workspaceMembers?.filter(
(workspaceMember) =>
!excludedWorkspaceMemberIds.includes(workspaceMember.id),
) ?? []) as WorkspaceMember[];
const handleSearchFilterChange = (event: ChangeEvent<HTMLInputElement>) => {
setSearchFilter(event.target.value);
};
return (
<DropdownMenuItemsContainer>
<DropdownMenuSearchInput
value={searchFilter}
onChange={handleSearchFilterChange}
placeholder="Search"
/>
<StyledWorkspaceMemberSelectContainer>
<RoleWorkspaceMemberPickerDropdownContent
loading={loading}
searchFilter={searchFilter}
filteredWorkspaceMembers={filteredWorkspaceMembers}
onSelect={onSelect}
/>
</StyledWorkspaceMemberSelectContainer>
</DropdownMenuItemsContainer>
);
};

View File

@ -0,0 +1,83 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { Avatar } from 'twenty-ui';
import { WorkspaceMember } from '~/generated-metadata/graphql';
const StyledEmptyState = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.light};
display: flex;
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.regular};
height: ${({ theme }) => theme.spacing(8)};
justify-content: flex-start;
padding: ${({ theme }) => theme.spacing(2)};
`;
const StyledWorkspaceMemberItem = styled.div`
align-items: center;
cursor: pointer;
display: flex;
font-size: ${({ theme }) => theme.font.size.md};
gap: ${({ theme }) => theme.spacing(2)};
min-width: ${({ theme }) => theme.spacing(45)};
padding: ${({ theme }) => theme.spacing(2)};
&:hover {
background: ${({ theme }) => theme.background.tertiary};
}
`;
const StyledWorkspaceMemberName = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
`;
type RoleWorkspaceMemberPickerDropdownContentProps = {
loading: boolean;
searchFilter: string;
filteredWorkspaceMembers: WorkspaceMember[];
onSelect: (workspaceMember: WorkspaceMember) => void;
};
export const RoleWorkspaceMemberPickerDropdownContent = ({
loading,
searchFilter,
filteredWorkspaceMembers,
onSelect,
}: RoleWorkspaceMemberPickerDropdownContentProps) => {
if (loading) {
return null;
}
if (!filteredWorkspaceMembers?.length) {
return (
<StyledEmptyState>
{searchFilter
? t`No members matching this search`
: t`No more members to add`}
</StyledEmptyState>
);
}
return (
<>
{filteredWorkspaceMembers.map((workspaceMember) => (
<StyledWorkspaceMemberItem
key={workspaceMember.id}
onClick={() => onSelect(workspaceMember)}
aria-label={`${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`}
>
<Avatar
type="rounded"
size="md"
placeholderColorSeed={workspaceMember.id}
placeholder={workspaceMember.name.firstName ?? ''}
/>
<StyledWorkspaceMemberName>
{workspaceMember.name.firstName} {workspaceMember.name.lastName}
</StyledWorkspaceMemberName>
</StyledWorkspaceMemberItem>
))}
</>
);
};

View File

@ -0,0 +1 @@
export type RoleAssignmentConfirmationModalMode = 'assign' | 'remove';

View File

@ -0,0 +1,5 @@
export type RoleAssignmentConfirmationModalSelectedWorkspaceMember = {
id: string;
name: string;
role?: { id: string; label: string };
};