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:
@ -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;
|
||||
}
|
||||
|
||||
@ -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'}>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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.`;
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export type RoleAssignmentConfirmationModalMode = 'assign' | 'remove';
|
||||
@ -0,0 +1,5 @@
|
||||
export type RoleAssignmentConfirmationModalSelectedWorkspaceMember = {
|
||||
id: string;
|
||||
name: string;
|
||||
role?: { id: string; label: string };
|
||||
};
|
||||
Reference in New Issue
Block a user