Merge pull request #30950 from overleaf/as-user-management-actions
[web] User management actions GitOrigin-RevId: c7683c596673bc252649cdeb975edd779a6154b0
This commit is contained in:
@@ -177,10 +177,26 @@ async function manageGroupUsers(req, res) {
|
||||
entityConfig
|
||||
)
|
||||
|
||||
const { usersNoInvites, invites } = Object.groupBy(users, user =>
|
||||
user.invite ? 'invites' : 'usersNoInvites'
|
||||
)
|
||||
|
||||
const deduplicatedUsers = _.uniqBy(usersNoInvites, 'email')
|
||||
for (const invite of invites) {
|
||||
const alreadyAdded = deduplicatedUsers.find(
|
||||
user => user.email === invite.email
|
||||
)
|
||||
if (alreadyAdded) {
|
||||
alreadyAdded.invite = true
|
||||
} else {
|
||||
deduplicatedUsers.unshift(invite)
|
||||
}
|
||||
}
|
||||
|
||||
res.render('user_membership/group-users-react', {
|
||||
name: entityName,
|
||||
groupId: entityPrimaryKey,
|
||||
users: _.uniqBy(users, 'email'),
|
||||
users: deduplicatedUsers,
|
||||
groupSize: subscription.membersLimit,
|
||||
managedUsersActive: subscription.managedUsersEnabled,
|
||||
entityAccess: UserMembershipAuthorization.hasEntityAccess()(req),
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
"all_projects": "",
|
||||
"all_projects_will_be_transferred_immediately": "",
|
||||
"all_these_experiments_are_available_exclusively": "",
|
||||
"allocate_license": "",
|
||||
"allows_to_search_by_author_title_etc_possible_to_pull_results_directly_from_your_reference_manager_if_connected": "",
|
||||
"an_email_has_already_been_sent_to": "",
|
||||
"an_error_occured_while_restoring_project": "",
|
||||
@@ -1564,6 +1565,7 @@
|
||||
"reviewing": "",
|
||||
"revoke": "",
|
||||
"revoke_invite": "",
|
||||
"revoke_license": "",
|
||||
"right": "",
|
||||
"role": "",
|
||||
"rolling_texlive_build": "",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames'
|
||||
import moment from 'moment'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { User } from '@ol-types/group-management/user'
|
||||
@@ -20,6 +20,18 @@ import getMeta from '@/utils/meta'
|
||||
|
||||
import BackButton from './back-button'
|
||||
import { useGroupMembersContext } from '../context/group-members-context'
|
||||
import OffboardManagedUserModal from './members-table/offboard-managed-user-modal'
|
||||
import RemoveManagedUserModal from './members-table/remove-managed-user-modal'
|
||||
import UnlinkUserModal from './members-table/unlink-user-modal'
|
||||
import { GroupUserAlert } from '../utils/types'
|
||||
import DropdownButton from './members-table/dropdown-button'
|
||||
import {
|
||||
OLModal,
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/shared/components/ol/ol-modal'
|
||||
|
||||
const getUserRole = (user: User) =>
|
||||
user.isEntityAdmin ? 'admin' : user.isEntityManager ? 'manager' : 'member'
|
||||
@@ -32,20 +44,27 @@ export default function GroupUsers() {
|
||||
const canUseAddSeatsFeature = getMeta('ol-canUseAddSeatsFeature')
|
||||
const groupSSOActive = getMeta('ol-groupSSOActive')
|
||||
const managedUsersActive = getMeta('ol-managedUsersActive')
|
||||
const hasWriteAccess = getMeta('ol-hasWriteAccess')
|
||||
const ownUserId = getMeta('ol-user_id')
|
||||
const [userToOffboard, setUserToOffboard] = useState<User | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [userToRemove, setUserToRemove] = useState<User | undefined>(undefined)
|
||||
const [userToUnlink, setUserToUnlink] = useState<User | undefined>(undefined)
|
||||
const [_groupUserAlert, setGroupUserAlert] =
|
||||
useState<GroupUserAlert>(undefined)
|
||||
|
||||
const {
|
||||
users,
|
||||
// selectedUsers,
|
||||
// addMembers,
|
||||
// removeMembers,
|
||||
// removeMemberLoading,
|
||||
// removeMemberError,
|
||||
// inviteMemberLoading,
|
||||
// inviteError,
|
||||
// memberAdded,
|
||||
// paths,
|
||||
selectedUsers,
|
||||
setSelectedUsers,
|
||||
selectUser,
|
||||
unselectUser,
|
||||
addManager,
|
||||
removeManager,
|
||||
addMembers,
|
||||
removeMember,
|
||||
} = useGroupMembersContext()
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
const numPages = Math.ceil(users.length / 10)
|
||||
|
||||
@@ -55,7 +74,62 @@ export default function GroupUsers() {
|
||||
return users.slice(firstUser, lastUser)
|
||||
}, [users, page])
|
||||
|
||||
const addedUsersSize = users.filter(user => user.isEntityMember).length
|
||||
const visibleNonMangedUsers = useMemo(
|
||||
() => paginatedUsers.filter(user => !user.enrollment?.managedBy),
|
||||
[paginatedUsers]
|
||||
)
|
||||
|
||||
const handleSelectAll = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedUsers(visibleNonMangedUsers)
|
||||
} else {
|
||||
setSelectedUsers([])
|
||||
}
|
||||
},
|
||||
[visibleNonMangedUsers, setSelectedUsers]
|
||||
)
|
||||
|
||||
const handleSelectUser = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>, user: User) => {
|
||||
if (e.target.checked) {
|
||||
selectUser(user)
|
||||
} else {
|
||||
unselectUser(user)
|
||||
}
|
||||
},
|
||||
[selectUser, unselectUser]
|
||||
)
|
||||
|
||||
const handleChangeRole = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>, user: User) => {
|
||||
if (e.target.value === 'manager') {
|
||||
addManager(user.email)
|
||||
} else if (e.target.value === 'member') {
|
||||
if (user.isEntityManager) {
|
||||
removeManager(user, true)
|
||||
}
|
||||
if (!user.isEntityMember && !user.invite) {
|
||||
addMembers(user.email)
|
||||
}
|
||||
}
|
||||
},
|
||||
[addManager, removeManager, addMembers]
|
||||
)
|
||||
|
||||
const handleRemoveUser = (user: User) => {
|
||||
if (user.isEntityManager) {
|
||||
removeManager(user)
|
||||
}
|
||||
if (user.isEntityMember || user.invite) {
|
||||
removeMember(user)
|
||||
}
|
||||
setUserToRemove(undefined)
|
||||
}
|
||||
|
||||
const allocatedLicenses = users.filter(
|
||||
user => user.isEntityMember || user.invite
|
||||
).length
|
||||
|
||||
return (
|
||||
<div className="container group-users-container">
|
||||
@@ -79,7 +153,9 @@ export default function GroupUsers() {
|
||||
</a>
|
||||
</OLCol>
|
||||
<OLCol xs="auto" className="align-content-center">
|
||||
<OLButton>{t('invite_users')}</OLButton>
|
||||
<OLButton disabled={allocatedLicenses <= groupSize}>
|
||||
{t('invite_users')}
|
||||
</OLButton>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
|
||||
@@ -87,12 +163,12 @@ export default function GroupUsers() {
|
||||
<OLNotification
|
||||
type="info"
|
||||
content={
|
||||
users.length === 1
|
||||
allocatedLicenses === 1
|
||||
? t('you_have_1_license_and_your_plan_supports_up_to_y', {
|
||||
groupSize,
|
||||
})
|
||||
: t('you_have_x_licenses_and_your_plan_supports_up_to_y', {
|
||||
addedUsersSize,
|
||||
addedUsersSize: allocatedLicenses,
|
||||
groupSize,
|
||||
})
|
||||
}
|
||||
@@ -101,7 +177,7 @@ export default function GroupUsers() {
|
||||
<a
|
||||
href="/user/subscription/group/add-users"
|
||||
className={classNames({
|
||||
'btn btn-premium': addedUsersSize === groupSize,
|
||||
'btn btn-premium': allocatedLicenses === groupSize,
|
||||
})}
|
||||
>
|
||||
{t('buy_more_licenses')}
|
||||
@@ -117,7 +193,16 @@ export default function GroupUsers() {
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<OLFormCheckbox />
|
||||
{hasWriteAccess && visibleNonMangedUsers.length > 0 && (
|
||||
<OLFormCheckbox
|
||||
autoComplete="off"
|
||||
onChange={handleSelectAll}
|
||||
checked={
|
||||
selectedUsers.length === visibleNonMangedUsers.length
|
||||
}
|
||||
aria-label={t('select_all')}
|
||||
/>
|
||||
)}
|
||||
</th>
|
||||
<th>{t('email')}</th>
|
||||
<th>{t('name')}</th>
|
||||
@@ -130,24 +215,34 @@ export default function GroupUsers() {
|
||||
{managedUsersActive && (
|
||||
<th className="text-center">{t('managed')}</th>
|
||||
)}
|
||||
<th />
|
||||
{hasWriteAccess && <th />}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paginatedUsers.map(user => (
|
||||
<tr key={user.email} className="align-middle">
|
||||
<td>
|
||||
<OLFormCheckbox />
|
||||
{hasWriteAccess &&
|
||||
!user.enrollment?.managedBy &&
|
||||
!user.isEntityAdmin &&
|
||||
user._id !== ownUserId && (
|
||||
<OLFormCheckbox
|
||||
autoComplete="off"
|
||||
onChange={e => handleSelectUser(e, user)}
|
||||
checked={selectedUsers.includes(user)}
|
||||
aria-label={t('select_user')}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>{user.email}</td>
|
||||
<td className="text-nowrap">
|
||||
{user.first_name} {user.last_name}
|
||||
</td>
|
||||
<td className="text-nowrap">
|
||||
{user.invite ? (
|
||||
<OLTag>{t('pending_invite')}</OLTag>
|
||||
) : (
|
||||
{user.last_active_at ? (
|
||||
moment(user.last_active_at).format('Do MMM YYYY')
|
||||
) : (
|
||||
<OLTag>{t('pending_invite')}</OLTag>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
@@ -156,9 +251,17 @@ export default function GroupUsers() {
|
||||
name="user-role"
|
||||
defaultValue={getUserRole(user)}
|
||||
className="user-role-select"
|
||||
disabled
|
||||
onChange={e => handleChangeRole(e, user)}
|
||||
disabled={
|
||||
!hasWriteAccess ||
|
||||
user.isEntityAdmin ||
|
||||
user._id === null ||
|
||||
user._id === ownUserId
|
||||
}
|
||||
>
|
||||
<option value="admin">{t('admin_titlecase')}</option>
|
||||
<option value="admin" hidden>
|
||||
{t('admin_titlecase')}
|
||||
</option>
|
||||
<option value="manager">{t('manager')}</option>
|
||||
<option value="member">{t('member')}</option>
|
||||
</OLFormSelect>
|
||||
@@ -170,6 +273,12 @@ export default function GroupUsers() {
|
||||
className="text-success"
|
||||
accessibilityLabel={t('license_allocated')}
|
||||
/>
|
||||
) : user.invite ? (
|
||||
<MaterialIcon
|
||||
type="schedule"
|
||||
className="text-warning"
|
||||
accessibilityLabel={t('pending_invite')}
|
||||
/>
|
||||
) : (
|
||||
<MaterialIcon
|
||||
type="close"
|
||||
@@ -214,7 +323,21 @@ export default function GroupUsers() {
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
<td>...</td>
|
||||
<td className="cell-dropdown">
|
||||
{hasWriteAccess &&
|
||||
!user.isEntityAdmin &&
|
||||
user._id !== ownUserId && (
|
||||
<DropdownButton
|
||||
user={user}
|
||||
openOffboardingModalForUser={setUserToOffboard}
|
||||
openRemoveModalForUser={setUserToRemove}
|
||||
openUnlinkUserModal={setUserToUnlink}
|
||||
setGroupUserAlert={setGroupUserAlert}
|
||||
groupId={groupId}
|
||||
combinedUserManagement
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -277,6 +400,53 @@ export default function GroupUsers() {
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
</OLCard>
|
||||
{userToOffboard && (
|
||||
<OffboardManagedUserModal
|
||||
user={userToOffboard}
|
||||
groupId={groupId}
|
||||
allMembers={users.filter(user => user.isEntityMember)}
|
||||
onClose={() => setUserToOffboard(undefined)}
|
||||
/>
|
||||
)}
|
||||
{userToRemove &&
|
||||
(userToRemove.enrollment?.managedBy === groupId ? (
|
||||
<RemoveManagedUserModal
|
||||
user={userToRemove}
|
||||
groupId={groupId}
|
||||
onClose={() => setUserToRemove(undefined)}
|
||||
/>
|
||||
) : (
|
||||
<OLModal
|
||||
show={userToRemove !== undefined}
|
||||
onHide={() => setUserToRemove(undefined)}
|
||||
>
|
||||
<OLModalHeader>
|
||||
<OLModalTitle>Remove user from group?</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
<OLModalBody>
|
||||
Are you sure you want to remove {userToRemove.email} from the
|
||||
group?
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton
|
||||
onClick={() => setUserToRemove(undefined)}
|
||||
variant="secondary"
|
||||
>
|
||||
Cancel
|
||||
</OLButton>
|
||||
<OLButton onClick={() => handleRemoveUser(userToRemove)}>
|
||||
Remove
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
))}
|
||||
{userToUnlink && (
|
||||
<UnlinkUserModal
|
||||
user={userToUnlink}
|
||||
onClose={() => setUserToUnlink(undefined)}
|
||||
setGroupUserAlert={setGroupUserAlert}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+55
-13
@@ -33,6 +33,7 @@ type ManagedUserDropdownButtonProps = {
|
||||
openUnlinkUserModal: (user: User) => void
|
||||
groupId: string
|
||||
setGroupUserAlert: Dispatch<SetStateAction<GroupUserAlert>>
|
||||
combinedUserManagement?: boolean
|
||||
}
|
||||
|
||||
export default function DropdownButton({
|
||||
@@ -42,9 +43,10 @@ export default function DropdownButton({
|
||||
openUnlinkUserModal,
|
||||
groupId,
|
||||
setGroupUserAlert,
|
||||
combinedUserManagement = false,
|
||||
}: ManagedUserDropdownButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
const { removeMember } = useGroupMembersContext()
|
||||
const { removeMember, addMembers } = useGroupMembersContext()
|
||||
const {
|
||||
runAsync: runResendManagedUserInviteAsync,
|
||||
isLoading: isResendingManagedUserInvite,
|
||||
@@ -183,13 +185,25 @@ export default function DropdownButton({
|
||||
}
|
||||
|
||||
const onRemoveFromGroup = () => {
|
||||
removeMember(user)
|
||||
if (combinedUserManagement) {
|
||||
openRemoveModalForUser(user)
|
||||
} else {
|
||||
removeMember(user)
|
||||
}
|
||||
}
|
||||
|
||||
const onUnlinkUserClick = () => {
|
||||
openUnlinkUserModal(user)
|
||||
}
|
||||
|
||||
const onAllocateLicenseClick = () => {
|
||||
addMembers(user.email)
|
||||
}
|
||||
|
||||
const onRevokeLicenseClick = () => {
|
||||
removeMember(user, combinedUserManagement)
|
||||
}
|
||||
|
||||
const buttons = []
|
||||
|
||||
if (userPending) {
|
||||
@@ -204,7 +218,12 @@ export default function DropdownButton({
|
||||
</MenuItemButton>
|
||||
)
|
||||
}
|
||||
if (managedUsersActive && !isUserManaged && !userPending) {
|
||||
if (
|
||||
managedUsersActive &&
|
||||
!isUserManaged &&
|
||||
!userPending &&
|
||||
user.isEntityMember
|
||||
) {
|
||||
buttons.push(
|
||||
<MenuItemButton
|
||||
onClick={onResendManagedUserInviteClick}
|
||||
@@ -216,6 +235,27 @@ export default function DropdownButton({
|
||||
</MenuItemButton>
|
||||
)
|
||||
}
|
||||
if (combinedUserManagement && user.isEntityManager) {
|
||||
if (!user.isEntityMember && !user.invite) {
|
||||
buttons.push(
|
||||
<MenuItemButton
|
||||
onClick={onAllocateLicenseClick}
|
||||
key="allocate-license-action"
|
||||
>
|
||||
{t('allocate_license')}
|
||||
</MenuItemButton>
|
||||
)
|
||||
} else {
|
||||
buttons.push(
|
||||
<MenuItemButton
|
||||
onClick={onRevokeLicenseClick}
|
||||
key="revoke-license-action"
|
||||
>
|
||||
{t('revoke_license')}
|
||||
</MenuItemButton>
|
||||
)
|
||||
}
|
||||
}
|
||||
if (groupSSOActive && isGroupSSOLinked) {
|
||||
buttons.push(
|
||||
<MenuItemButton
|
||||
@@ -244,15 +284,6 @@ export default function DropdownButton({
|
||||
!user.isEntityAdmin &&
|
||||
(!isUserGroupManager || userId !== user._id)
|
||||
) {
|
||||
buttons.push(
|
||||
<MenuItemButton
|
||||
key="delete-user-action"
|
||||
data-testid="delete-user-action"
|
||||
onClick={onDeleteUserClick}
|
||||
>
|
||||
{t('delete_permanently')}
|
||||
</MenuItemButton>
|
||||
)
|
||||
buttons.push(
|
||||
<MenuItemButton
|
||||
key="release-user-action"
|
||||
@@ -262,6 +293,15 @@ export default function DropdownButton({
|
||||
{t('remove_from_group')}
|
||||
</MenuItemButton>
|
||||
)
|
||||
buttons.push(
|
||||
<MenuItemButton
|
||||
key="delete-user-action"
|
||||
data-testid="delete-user-action"
|
||||
onClick={onDeleteUserClick}
|
||||
>
|
||||
{t('delete_permanently')}
|
||||
</MenuItemButton>
|
||||
)
|
||||
} else if (!isUserManaged) {
|
||||
buttons.push(
|
||||
<MenuItemButton
|
||||
@@ -298,7 +338,9 @@ export default function DropdownButton({
|
||||
>
|
||||
<MaterialIcon type="more_vert" accessibilityLabel={t('actions')} />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu flip={false}>{buttons}</DropdownMenu>
|
||||
<DropdownMenu flip renderOnMount popperConfig={{ strategy: 'fixed' }}>
|
||||
{buttons}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
+120
-20
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
Dispatch,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
@@ -18,19 +20,22 @@ import { debugConsole } from '@/utils/debugging'
|
||||
export type GroupMembersContextValue = {
|
||||
users: User[]
|
||||
selectedUsers: User[]
|
||||
setSelectedUsers: Dispatch<SetStateAction<User[]>>
|
||||
selectUser: (user: User) => void
|
||||
selectAllUsers: () => void
|
||||
unselectAllUsers: () => void
|
||||
selectAllNonManagedUsers: () => void
|
||||
unselectUser: (user: User) => void
|
||||
addMembers: (emailString: string) => void
|
||||
removeMembers: (e: any) => void
|
||||
removeMember: (user: User) => Promise<void>
|
||||
removeMembers: (e: any, keepUsers?: boolean) => void
|
||||
removeMember: (user: User, keepUser?: boolean) => Promise<void>
|
||||
removeMemberLoading: boolean
|
||||
removeMemberError?: APIError
|
||||
updateMemberView: (userId: string, updatedUser: User) => void
|
||||
inviteMemberLoading: boolean
|
||||
inviteError?: APIError
|
||||
addManager: (email: string) => Promise<void>
|
||||
removeManager: (user: User, keepUser?: boolean) => Promise<void>
|
||||
memberAdded: boolean
|
||||
paths: { [key: string]: string }
|
||||
}
|
||||
@@ -48,6 +53,7 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
|
||||
users,
|
||||
setUsers,
|
||||
selectedUsers,
|
||||
setSelectedUsers,
|
||||
selectAllUsers,
|
||||
unselectAllUsers,
|
||||
selectAllNonManagedUsers,
|
||||
@@ -59,6 +65,8 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
|
||||
const [inviteError, setInviteError] = useState<APIError>()
|
||||
const [removeMemberInflightCount, setRemoveMemberInflightCount] = useState(0)
|
||||
const [removeMemberError, setRemoveMemberError] = useState<APIError>()
|
||||
const [addManagerError, setAddManagerError] = useState<APIError>()
|
||||
const [removeManagerError, setRemoveManagerError] = useState<APIError>()
|
||||
const [memberAdded, setMemberAdded] = useState(false)
|
||||
|
||||
const groupId = getMeta('ol-groupId')
|
||||
@@ -69,10 +77,27 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
|
||||
removeMember: `/manage/groups/${groupId}/user`,
|
||||
removeInvite: `/manage/groups/${groupId}/invites`,
|
||||
exportMembers: `/manage/groups/${groupId}/members/export`,
|
||||
addManager: `/manage/groups/${groupId}/managers`,
|
||||
removeManager: `/manage/groups/${groupId}/managers`,
|
||||
}),
|
||||
[groupId]
|
||||
)
|
||||
|
||||
const updateMemberView = useCallback(
|
||||
(userId: string, updatedUser: User) => {
|
||||
setUsers(users =>
|
||||
users.map(u => {
|
||||
if (u._id === userId) {
|
||||
return updatedUser
|
||||
} else {
|
||||
return u
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
[setUsers]
|
||||
)
|
||||
|
||||
const addMembers = useCallback(
|
||||
(emailString: string) => {
|
||||
setInviteError(undefined)
|
||||
@@ -93,6 +118,11 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
|
||||
)
|
||||
if (!alreadyListed) {
|
||||
setUsers(users => [...users, data.user])
|
||||
} else {
|
||||
updateMemberView(alreadyListed._id, {
|
||||
...alreadyListed,
|
||||
invite: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
@@ -109,13 +139,13 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
|
||||
})
|
||||
})
|
||||
},
|
||||
[paths.addMember, users, setUsers]
|
||||
[paths.addMember, users, setUsers, updateMemberView]
|
||||
)
|
||||
|
||||
const removeMember = useCallback(
|
||||
async (user: User) => {
|
||||
async (user: User, keepUser = false) => {
|
||||
let url
|
||||
if (paths.removeInvite && user.invite && user._id == null) {
|
||||
if (paths.removeInvite && user.invite && user._id === null) {
|
||||
url = `${paths.removeInvite}/${encodeURIComponent(user.email)}`
|
||||
} else if (paths.removeMember && user._id) {
|
||||
url = `${paths.removeMember}/${user._id}`
|
||||
@@ -125,7 +155,16 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
|
||||
setRemoveMemberInflightCount(count => count + 1)
|
||||
try {
|
||||
await deleteJSON(url, {})
|
||||
setUsers(users => users.filter(u => u !== user))
|
||||
if (!keepUser) {
|
||||
setUsers(users => users.filter(u => u !== user))
|
||||
} else {
|
||||
updateMemberView(user._id, {
|
||||
...user,
|
||||
invite: false,
|
||||
isEntityMember: false,
|
||||
enrollment: {},
|
||||
})
|
||||
}
|
||||
unselectUser(user)
|
||||
} catch (error: unknown) {
|
||||
debugConsole.error(error)
|
||||
@@ -133,11 +172,17 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
|
||||
}
|
||||
setRemoveMemberInflightCount(count => count - 1)
|
||||
},
|
||||
[unselectUser, setUsers, paths.removeInvite, paths.removeMember]
|
||||
[
|
||||
unselectUser,
|
||||
setUsers,
|
||||
paths.removeInvite,
|
||||
paths.removeMember,
|
||||
updateMemberView,
|
||||
]
|
||||
)
|
||||
|
||||
const removeMembers = useCallback(
|
||||
(e: any) => {
|
||||
(e: any, keepUsers = false) => {
|
||||
e.preventDefault()
|
||||
setRemoveMemberError(undefined)
|
||||
;(async () => {
|
||||
@@ -145,32 +190,78 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
|
||||
if (user?.enrollment?.managedBy) {
|
||||
continue
|
||||
}
|
||||
await removeMember(user)
|
||||
await removeMember(user, keepUsers)
|
||||
}
|
||||
})()
|
||||
},
|
||||
[selectedUsers, removeMember]
|
||||
)
|
||||
|
||||
const updateMemberView = useCallback(
|
||||
(userId: string, updatedUser: User) => {
|
||||
setUsers(
|
||||
users.map(u => {
|
||||
if (u._id === userId) {
|
||||
return updatedUser
|
||||
} else {
|
||||
return u
|
||||
}
|
||||
const addManager = useCallback(
|
||||
async (email: string) => {
|
||||
setAddManagerError(undefined)
|
||||
|
||||
try {
|
||||
const data = await postJSON<{ user: User }>(paths.addManager, {
|
||||
body: {
|
||||
email,
|
||||
},
|
||||
})
|
||||
)
|
||||
if (data.user) {
|
||||
const alreadyListed = users.find(
|
||||
user => user.email === data.user.email
|
||||
)
|
||||
if (!alreadyListed) {
|
||||
setUsers(users => [...users, data.user])
|
||||
} else {
|
||||
updateMemberView(alreadyListed._id, {
|
||||
...alreadyListed,
|
||||
isEntityManager: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
debugConsole.error(error)
|
||||
setAddManagerError((error as FetchError)?.data?.error || {})
|
||||
}
|
||||
},
|
||||
[setUsers, users]
|
||||
[paths.addManager, users, setUsers, updateMemberView]
|
||||
)
|
||||
|
||||
const removeManager = useCallback(
|
||||
async (user: User, keepUser = false) => {
|
||||
setRemoveMemberError(undefined)
|
||||
|
||||
let url
|
||||
if (paths.removeManager && user._id) {
|
||||
url = `${paths.removeManager}/${user._id}`
|
||||
} else {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await deleteJSON(url, {})
|
||||
if (!keepUser) {
|
||||
setUsers(users => users.filter(u => u !== user))
|
||||
} else {
|
||||
updateMemberView(user._id, {
|
||||
...user,
|
||||
isEntityManager: false,
|
||||
})
|
||||
}
|
||||
unselectUser(user)
|
||||
} catch (error: unknown) {
|
||||
debugConsole.error(error)
|
||||
setRemoveManagerError((error as FetchError)?.data?.error || {})
|
||||
}
|
||||
},
|
||||
[unselectUser, paths.removeManager, setUsers, updateMemberView]
|
||||
)
|
||||
|
||||
const value = useMemo<GroupMembersContextValue>(
|
||||
() => ({
|
||||
users,
|
||||
selectedUsers,
|
||||
setSelectedUsers,
|
||||
selectAllUsers,
|
||||
unselectAllUsers,
|
||||
selectAllNonManagedUsers,
|
||||
@@ -184,11 +275,16 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
|
||||
removeMemberError,
|
||||
inviteMemberLoading: inviteUserInflightCount > 0,
|
||||
inviteError,
|
||||
addManager,
|
||||
addManagerError,
|
||||
removeManager,
|
||||
removeManagerError,
|
||||
memberAdded,
|
||||
paths,
|
||||
}),
|
||||
[
|
||||
users,
|
||||
setSelectedUsers,
|
||||
selectedUsers,
|
||||
selectAllUsers,
|
||||
unselectAllUsers,
|
||||
@@ -203,6 +299,10 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
|
||||
removeMemberError,
|
||||
inviteUserInflightCount,
|
||||
inviteError,
|
||||
addManager,
|
||||
addManagerError,
|
||||
removeManager,
|
||||
removeManagerError,
|
||||
memberAdded,
|
||||
paths,
|
||||
]
|
||||
|
||||
@@ -28,6 +28,7 @@ export default function useUserSelection(initialUsers: User[]) {
|
||||
users,
|
||||
setUsers,
|
||||
selectedUsers,
|
||||
setSelectedUsers,
|
||||
selectUser,
|
||||
unselectUser,
|
||||
selectAllUsers,
|
||||
|
||||
@@ -169,6 +169,7 @@
|
||||
"all_templates": "All templates",
|
||||
"all_the_pros_of_our_standard_plan_plus_unlimited_collab": "All the pros of our standard plan, plus unlimited collaborators per project.",
|
||||
"all_these_experiments_are_available_exclusively": "All these experiments are available exclusively to members of the Labs program. If you sign up, you can choose which experiments you want to try.",
|
||||
"allocate_license": "Allocate license",
|
||||
"allows_to_search_by_author_title_etc_possible_to_pull_results_directly_from_your_reference_manager_if_connected": "Allows to search by author, title, etc. Possible to pull results directly from your reference manager (if connected).",
|
||||
"already_have_an_account": "Already have an account?",
|
||||
"already_have_sl_account": "Already have an __appName__ account?",
|
||||
@@ -2014,6 +2015,7 @@
|
||||
"reviewing": "Reviewing",
|
||||
"revoke": "Revoke",
|
||||
"revoke_invite": "Revoke Invite",
|
||||
"revoke_license": "Revoke license",
|
||||
"right": "Right",
|
||||
"ro": "Romanian",
|
||||
"role": "Role",
|
||||
|
||||
+2
-2
@@ -74,7 +74,7 @@ describe('GroupUsers', function () {
|
||||
|
||||
it('displays license information correctly', function () {
|
||||
cy.get('.license-info').contains(
|
||||
'You have allocated 2 licenses and your plan supports up to 10'
|
||||
'You have allocated 3 licenses and your plan supports up to 10'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -110,7 +110,7 @@ describe('GroupUsers', function () {
|
||||
'have.value',
|
||||
'member'
|
||||
)
|
||||
cy.contains('License not allocated')
|
||||
cy.contains('Pending invite')
|
||||
})
|
||||
|
||||
cy.get('tr:nth-child(4)').within(() => {
|
||||
|
||||
+5
@@ -210,6 +210,7 @@ describe('DropdownButton', function () {
|
||||
last_active_at: new Date(),
|
||||
enrollment: {},
|
||||
isEntityAdmin: undefined,
|
||||
isEntityMember: true,
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
@@ -451,6 +452,7 @@ describe('DropdownButton', function () {
|
||||
],
|
||||
},
|
||||
isEntityAdmin: undefined,
|
||||
isEntityMember: true,
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
@@ -493,6 +495,7 @@ describe('DropdownButton', function () {
|
||||
sso: [],
|
||||
},
|
||||
isEntityAdmin: undefined,
|
||||
isEntityMember: true,
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
@@ -542,6 +545,7 @@ describe('DropdownButton', function () {
|
||||
],
|
||||
},
|
||||
isEntityAdmin: true,
|
||||
isEntityMember: true,
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
@@ -587,6 +591,7 @@ describe('DropdownButton', function () {
|
||||
sso: [],
|
||||
},
|
||||
isEntityAdmin: true,
|
||||
isEntityMember: true,
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
|
||||
Reference in New Issue
Block a user