import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { Trans, useLingui } from '@lingui/react/macro'; import { isNonEmptyArray } from '@sniptt/guards'; import { useState } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPath } from '@/types/SettingsPath'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { TextInput } from '@/ui/input/components/TextInput'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { useModal } from '@/ui/layout/modal/hooks/useModal'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Table } from '@/ui/layout/table/components/Table'; import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; import { WorkspaceInviteLink } from '@/workspace/components/WorkspaceInviteLink'; import { WorkspaceInviteTeam } from '@/workspace/components/WorkspaceInviteTeam'; import { formatDistanceToNow } from 'date-fns'; import { isDefined } from 'twenty-shared/utils'; import { AppTooltip, Avatar, H2Title, IconMail, IconReload, IconSearch, IconTrash, Status, TooltipDelay, } from 'twenty-ui/display'; import { IconButton } from 'twenty-ui/input'; import { Section } from 'twenty-ui/layout'; import { useGetWorkspaceInvitationsQuery } from '~/generated/graphql'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; import { TableCell } from '../../modules/ui/layout/table/components/TableCell'; import { TableRow } from '../../modules/ui/layout/table/components/TableRow'; import { useDeleteWorkspaceInvitation } from '../../modules/workspace-invitation/hooks/useDeleteWorkspaceInvitation'; import { useResendWorkspaceInvitation } from '../../modules/workspace-invitation/hooks/useResendWorkspaceInvitation'; import { workspaceInvitationsState } from '../../modules/workspace-invitation/states/workspaceInvitationsStates'; export const WORKSPACE_MEMBER_DELETION_MODAL_ID = 'workspace-member-deletion-modal'; const StyledButtonContainer = styled.div` align-items: center; display: flex; flex-direction: row; margin-left: ${({ theme }) => theme.spacing(3)}; `; const StyledTable = styled(Table)` border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; `; const StyledIconWrapper = styled.div` display: flex; align-items: center; margin-right: ${({ theme }) => theme.spacing(2)}; `; const StyledTextContainerWithEllipsis = styled.div` overflow: hidden; text-overflow: ellipsis; white-space: nowrap; `; const StyledSearchContainer = styled.div` padding-bottom: ${({ theme }) => theme.spacing(2)}; `; const StyledSearchInput = styled(TextInput)` input { background: ${({ theme }) => theme.background.transparent.lighter}; border: 1px solid ${({ theme }) => theme.border.color.medium}; } `; const StyledTableRows = styled.div` padding-bottom: ${({ theme }) => theme.spacing(2)}; padding-top: ${({ theme }) => theme.spacing(2)}; `; const StyledNoMembers = styled(TableCell)` color: ${({ theme }) => theme.font.color.tertiary}; `; export const SettingsWorkspaceMembers = () => { const { t } = useLingui(); const { enqueueSnackBar } = useSnackBar(); const theme = useTheme(); const [workspaceMemberToDelete, setWorkspaceMemberToDelete] = useState< string | undefined >(); const { records: workspaceMembers } = useFindManyRecords({ objectNameSingular: CoreObjectNameSingular.WorkspaceMember, }); const { deleteOneRecord: deleteOneWorkspaceMember } = useDeleteOneRecord({ objectNameSingular: CoreObjectNameSingular.WorkspaceMember, }); const { resendInvitation } = useResendWorkspaceInvitation(); const { deleteWorkspaceInvitation } = useDeleteWorkspaceInvitation(); const currentWorkspace = useRecoilValue(currentWorkspaceState); const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const handleRemoveWorkspaceMember = async (workspaceMemberId: string) => { await deleteOneWorkspaceMember?.(workspaceMemberId); }; const workspaceInvitations = useRecoilValue(workspaceInvitationsState); const setWorkspaceInvitations = useSetRecoilState(workspaceInvitationsState); const [searchFilter, setSearchFilter] = useState(''); const handleSearchChange = (text: string) => { setSearchFilter(text); }; useGetWorkspaceInvitationsQuery({ onError: (error: Error) => { enqueueSnackBar(error.message, { variant: SnackBarVariant.Error, }); }, onCompleted: (data) => { setWorkspaceInvitations(data?.findWorkspaceInvitations ?? []); }, }); const handleRemoveWorkspaceInvitation = async (appTokenId: string) => { const result = await deleteWorkspaceInvitation({ appTokenId }); if (isDefined(result.errors)) { enqueueSnackBar(t`Error deleting invitation`, { variant: SnackBarVariant.Error, duration: 2000, }); } }; const handleResendWorkspaceInvitation = async (appTokenId: string) => { const result = await resendInvitation({ appTokenId }); if (isDefined(result.errors)) { enqueueSnackBar(t`Error resending invitation`, { variant: SnackBarVariant.Error, duration: 2000, }); } }; const getExpiresAtText = (expiresAt: string) => { const expiresAtDate = new Date(expiresAt); return expiresAtDate < new Date() ? t`Expired` : formatDistanceToNow(new Date(expiresAt)); }; const filteredWorkspaceMembers = !searchFilter ? workspaceMembers : 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 { openModal } = useModal(); return ( Workspace, href: getSettingsPath(SettingsPath.Workspace), }, { children: Members }, ]} > {currentWorkspace?.inviteHash && currentWorkspace?.isPublicInviteLinkEnabled && (
)}
Name Email {filteredWorkspaceMembers.length > 0 ? ( filteredWorkspaceMembers.map((workspaceMember) => ( {workspaceMember.name.firstName + ' ' + workspaceMember.name.lastName} {workspaceMember.userEmail} {currentWorkspaceMember?.id !== workspaceMember.id && ( { openModal(WORKSPACE_MEMBER_DELETION_MODAL_ID); setWorkspaceMemberToDelete(workspaceMember.id); }} variant="tertiary" size="medium" Icon={IconTrash} /> )} )) ) : ( {!searchFilter ? t`No members` : t`No members match your search`} )}
{isNonEmptyArray(workspaceInvitations) && ( Email Expires in {workspaceInvitations?.map((workspaceInvitation) => ( {workspaceInvitation.email} { handleResendWorkspaceInvitation( workspaceInvitation.id, ); }} variant="tertiary" size="medium" Icon={IconReload} /> { handleRemoveWorkspaceInvitation( workspaceInvitation.id, ); }} variant="tertiary" size="medium" Icon={IconTrash} /> ))} )}
This action cannot be undone. This will permanently delete this user and remove them from all their assignments. } onConfirmClick={() => workspaceMemberToDelete && handleRemoveWorkspaceMember(workspaceMemberToDelete) } confirmButtonText={t`Delete account`} />
); };