mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-01 19:54:46 +00:00
Merge pull request #14256 from overleaf/ab-unmanaged-remove-from-group-dropdown-action
[web] Add Remove from group action in dropdown for unmanaged/pending users GitOrigin-RevId: daa66598e42befa2f8430bdf118e907a8758d60e
This commit is contained in:
parent
d73f77e88e
commit
a4a5a08c31
17 changed files with 445 additions and 348 deletions
|
@ -1,25 +1,20 @@
|
|||
import { Col, Row } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { User } from '../../../../../types/group-management/user'
|
||||
import Tooltip from '../../../shared/components/tooltip'
|
||||
import { useGroupMembersContext } from '../context/group-members-context'
|
||||
import GroupMemberRow from './group-member-row'
|
||||
|
||||
type GroupMembersListProps = {
|
||||
handleSelectAllClick: (e: any) => void
|
||||
selectedUsers: User[]
|
||||
users: User[]
|
||||
selectUser: (user: User) => void
|
||||
unselectUser: (user: User) => void
|
||||
}
|
||||
|
||||
export default function GroupMembersList({
|
||||
handleSelectAllClick,
|
||||
selectedUsers,
|
||||
users,
|
||||
selectUser,
|
||||
unselectUser,
|
||||
}: GroupMembersListProps) {
|
||||
const { t } = useTranslation()
|
||||
const { selectedUsers, users, selectUser, unselectUser } =
|
||||
useGroupMembersContext()
|
||||
|
||||
return (
|
||||
<ul className="list-unstyled structured-list">
|
||||
<li className="container-fluid">
|
||||
|
|
|
@ -1,129 +1,38 @@
|
|||
import { useCallback, useMemo, useState } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { Button, Col, Form, FormControl, Row } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { User } from '../../../../../types/group-management/user'
|
||||
import {
|
||||
deleteJSON,
|
||||
FetchError,
|
||||
postJSON,
|
||||
} from '../../../infrastructure/fetch-json'
|
||||
import MaterialIcon from '../../../shared/components/material-icon'
|
||||
import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n'
|
||||
import getMeta from '../../../utils/meta'
|
||||
import { parseEmails } from '../utils/emails'
|
||||
import ErrorAlert, { APIError } from './error-alert'
|
||||
import useUserSelection from '../hooks/use-user-selection'
|
||||
import { useGroupMembersContext } from '../context/group-members-context'
|
||||
import ErrorAlert from './error-alert'
|
||||
import ManagedUsersList from './managed-users/managed-users-list'
|
||||
import GroupMembersList from './group-members-list'
|
||||
|
||||
export default function GroupMembers() {
|
||||
const { isReady } = useWaitForI18n()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
users,
|
||||
setUsers,
|
||||
selectedUsers,
|
||||
selectAllUsers,
|
||||
unselectAllUsers,
|
||||
selectUser,
|
||||
unselectUser,
|
||||
} = useUserSelection(getMeta('ol-users', []))
|
||||
|
||||
addMembers,
|
||||
removeMembers,
|
||||
removeMemberLoading,
|
||||
removeMemberError,
|
||||
inviteMemberLoading,
|
||||
inviteError,
|
||||
paths,
|
||||
} = useGroupMembersContext()
|
||||
const [emailString, setEmailString] = useState<string>('')
|
||||
const [inviteUserInflightCount, setInviteUserInflightCount] = useState(0)
|
||||
const [inviteError, setInviteError] = useState<APIError>()
|
||||
const [removeMemberInflightCount, setRemoveMemberInflightCount] = useState(0)
|
||||
const [removeMemberError, setRemoveMemberError] = useState<APIError>()
|
||||
|
||||
const groupId: string = getMeta('ol-groupId')
|
||||
const groupName: string = getMeta('ol-groupName')
|
||||
const groupSize: number = getMeta('ol-groupSize')
|
||||
const managedUsersActive: any = getMeta('ol-managedUsersActive')
|
||||
|
||||
const paths = useMemo(
|
||||
() => ({
|
||||
addMember: `/manage/groups/${groupId}/invites`,
|
||||
removeMember: `/manage/groups/${groupId}/user`,
|
||||
removeInvite: `/manage/groups/${groupId}/invites`,
|
||||
exportMembers: `/manage/groups/${groupId}/members/export`,
|
||||
}),
|
||||
[groupId]
|
||||
)
|
||||
|
||||
const addMembers = useCallback(
|
||||
e => {
|
||||
e.preventDefault()
|
||||
setInviteError(undefined)
|
||||
const emails = parseEmails(emailString)
|
||||
;(async () => {
|
||||
for (const email of emails) {
|
||||
setInviteUserInflightCount(count => count + 1)
|
||||
try {
|
||||
const data = await postJSON<{ user: User }>(paths.addMember, {
|
||||
body: {
|
||||
email,
|
||||
},
|
||||
})
|
||||
if (data.user) {
|
||||
const alreadyListed = users.find(
|
||||
user => user.email === data.user.email
|
||||
)
|
||||
if (!alreadyListed) {
|
||||
setUsers(users => [...users, data.user])
|
||||
}
|
||||
}
|
||||
setEmailString('')
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
setInviteError((error as FetchError)?.data?.error || {})
|
||||
}
|
||||
setInviteUserInflightCount(count => count - 1)
|
||||
}
|
||||
})()
|
||||
},
|
||||
[emailString, paths.addMember, users, setUsers]
|
||||
)
|
||||
|
||||
const removeMembers = useCallback(
|
||||
e => {
|
||||
e.preventDefault()
|
||||
setRemoveMemberError(undefined)
|
||||
;(async () => {
|
||||
for (const user of selectedUsers) {
|
||||
if (user?.enrollment?.managedBy) {
|
||||
continue
|
||||
}
|
||||
let url
|
||||
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}`
|
||||
} else {
|
||||
return
|
||||
}
|
||||
setRemoveMemberInflightCount(count => count + 1)
|
||||
try {
|
||||
await deleteJSON(url, {})
|
||||
setUsers(users => users.filter(u => u !== user))
|
||||
unselectUser(user)
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
setRemoveMemberError((error as FetchError)?.data?.error || {})
|
||||
}
|
||||
setRemoveMemberInflightCount(count => count - 1)
|
||||
}
|
||||
})()
|
||||
},
|
||||
[
|
||||
selectedUsers,
|
||||
unselectUser,
|
||||
setUsers,
|
||||
paths.removeInvite,
|
||||
paths.removeMember,
|
||||
]
|
||||
)
|
||||
|
||||
const handleSelectAllClick = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked) {
|
||||
|
@ -153,6 +62,11 @@ export default function GroupMembers() {
|
|||
)
|
||||
}
|
||||
|
||||
const onAddMembersSubmit = (e: React.FormEvent<Form>) => {
|
||||
e.preventDefault()
|
||||
addMembers(emailString)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<Row>
|
||||
|
@ -178,7 +92,7 @@ export default function GroupMembers() {
|
|||
/>
|
||||
</small>
|
||||
)}
|
||||
{removeMemberInflightCount > 0 ? (
|
||||
{removeMemberLoading ? (
|
||||
<Button bsStyle="danger" disabled>
|
||||
{t('removing')}…
|
||||
</Button>
|
||||
|
@ -199,20 +113,10 @@ export default function GroupMembers() {
|
|||
{managedUsersActive ? (
|
||||
<ManagedUsersList
|
||||
handleSelectAllClick={handleSelectAllClick}
|
||||
selectedUsers={selectedUsers}
|
||||
users={users}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
groupId={groupId}
|
||||
/>
|
||||
) : (
|
||||
<GroupMembersList
|
||||
handleSelectAllClick={handleSelectAllClick}
|
||||
selectedUsers={selectedUsers}
|
||||
users={users}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
/>
|
||||
<GroupMembersList handleSelectAllClick={handleSelectAllClick} />
|
||||
)}
|
||||
</div>
|
||||
<hr />
|
||||
|
@ -220,7 +124,7 @@ export default function GroupMembers() {
|
|||
<div className="add-more-members-form">
|
||||
<p className="small">{t('add_more_members')}</p>
|
||||
<ErrorAlert error={inviteError} />
|
||||
<Form horizontal onSubmit={addMembers} className="form">
|
||||
<Form horizontal onSubmit={onAddMembersSubmit} className="form">
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<FormControl
|
||||
|
@ -232,12 +136,12 @@ export default function GroupMembers() {
|
|||
/>
|
||||
</Col>
|
||||
<Col xs={4}>
|
||||
{inviteUserInflightCount > 0 ? (
|
||||
{inviteMemberLoading ? (
|
||||
<Button bsStyle="primary" disabled>
|
||||
{t('adding')}…
|
||||
</Button>
|
||||
) : (
|
||||
<Button bsStyle="primary" onClick={addMembers}>
|
||||
<Button bsStyle="primary" onClick={onAddMembersSubmit}>
|
||||
{t('add')}
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { User } from '../../../../../../types/group-management/user'
|
|||
import ControlledDropdown from '../../../../shared/components/controlled-dropdown'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MenuItemButton from '../../../project-list/components/dropdown/menu-item-button'
|
||||
import { useGroupMembersContext } from '../../context/group-members-context'
|
||||
|
||||
type ManagedUserDropdownButtonProps = {
|
||||
user: User
|
||||
|
@ -14,14 +15,19 @@ export default function ManagedUserDropdownButton({
|
|||
openOffboardingModalForUser,
|
||||
}: ManagedUserDropdownButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
const { removeMember } = useGroupMembersContext()
|
||||
|
||||
const onDeleteUserClick = () => {
|
||||
openOffboardingModalForUser(user)
|
||||
}
|
||||
|
||||
const onRemoveFromGroup = () => {
|
||||
removeMember(user)
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="managed-user-actions">
|
||||
<ControlledDropdown id={`managed-user-dropdown-${user._id}`}>
|
||||
<ControlledDropdown id={`managed-user-dropdown-${user.email}`}>
|
||||
<Dropdown.Toggle
|
||||
bsStyle={null}
|
||||
className="btn btn-link action-btn"
|
||||
|
@ -33,18 +39,27 @@ export default function ManagedUserDropdownButton({
|
|||
aria-label={t('actions')}
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="dropdown-menu-right">
|
||||
<Dropdown.Menu className="dropdown-menu-right managed-users-dropdown-menu">
|
||||
{user.enrollment ? (
|
||||
<MenuItemButton
|
||||
className="delete-user-action"
|
||||
data-testid="delete-user-action"
|
||||
onClick={onDeleteUserClick}
|
||||
>
|
||||
{t('delete_user')}
|
||||
</MenuItemButton>
|
||||
) : (
|
||||
<MenuItem className="no-actions-available">
|
||||
) : user.isEntityAdmin ? (
|
||||
<MenuItem data-testid="no-actions-available">
|
||||
<span className="text-muted">{t('no_actions')}</span>
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItemButton
|
||||
onClick={onRemoveFromGroup}
|
||||
className="delete-user-action"
|
||||
data-testid="remove-user-action"
|
||||
>
|
||||
{t('remove_from_group')}
|
||||
</MenuItemButton>
|
||||
)}
|
||||
</Dropdown.Menu>
|
||||
</ControlledDropdown>
|
||||
|
|
|
@ -4,26 +4,22 @@ import { Col, Row } from 'react-bootstrap'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { User } from '../../../../../../types/group-management/user'
|
||||
import Badge from '../../../../shared/components/badge'
|
||||
import { useGroupMembersContext } from '../../context/group-members-context'
|
||||
import ManagedUserDropdownButton from './managed-user-dropdown-button'
|
||||
import Tooltip from '../../../../shared/components/tooltip'
|
||||
import ManagedUserStatus from './managed-user-status'
|
||||
|
||||
type ManagedUserRowProps = {
|
||||
user: User
|
||||
selectUser: (user: User) => void
|
||||
unselectUser: (user: User) => void
|
||||
selected: boolean
|
||||
openOffboardingModalForUser: (user: User) => void
|
||||
}
|
||||
|
||||
export default function ManagedUserRow({
|
||||
user,
|
||||
selectUser,
|
||||
unselectUser,
|
||||
selected,
|
||||
openOffboardingModalForUser,
|
||||
}: ManagedUserRowProps) {
|
||||
const { t } = useTranslation()
|
||||
const { selectedUsers, selectUser, unselectUser } = useGroupMembersContext()
|
||||
|
||||
const handleSelectUser = useCallback(
|
||||
(event, user) => {
|
||||
|
@ -36,6 +32,8 @@ export default function ManagedUserRow({
|
|||
[selectUser, unselectUser]
|
||||
)
|
||||
|
||||
const selected = selectedUsers.includes(user)
|
||||
|
||||
return (
|
||||
<li
|
||||
key={`user-${user.email}`}
|
||||
|
|
|
@ -2,31 +2,25 @@ import { Col, Row } from 'react-bootstrap'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { User } from '../../../../../../types/group-management/user'
|
||||
import Tooltip from '../../../../shared/components/tooltip'
|
||||
import { useGroupMembersContext } from '../../context/group-members-context'
|
||||
import ManagedUserRow from './managed-user-row'
|
||||
import OffboardManagedUserModal from './offboard-managed-user-modal'
|
||||
import { useState } from 'react'
|
||||
|
||||
type ManagedUsersListProps = {
|
||||
handleSelectAllClick: (e: any) => void
|
||||
selectedUsers: User[]
|
||||
users: User[]
|
||||
selectUser: (user: User) => void
|
||||
unselectUser: (user: User) => void
|
||||
groupId: string
|
||||
}
|
||||
|
||||
export default function ManagedUsersList({
|
||||
handleSelectAllClick,
|
||||
selectedUsers,
|
||||
users,
|
||||
selectUser,
|
||||
unselectUser,
|
||||
groupId,
|
||||
}: ManagedUsersListProps) {
|
||||
const { t } = useTranslation()
|
||||
const [userToOffboard, setUserToOffboard] = useState<User | undefined>(
|
||||
undefined
|
||||
)
|
||||
const { selectedUsers, users } = useGroupMembersContext()
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -81,9 +75,6 @@ export default function ManagedUsersList({
|
|||
<ManagedUserRow
|
||||
key={user.email}
|
||||
user={user}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
selected={selectedUsers.includes(user)}
|
||||
openOffboardingModalForUser={setUserToOffboard}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -0,0 +1,194 @@
|
|||
import {
|
||||
ReactNode,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { User } from '../../../../../types/group-management/user'
|
||||
import {
|
||||
deleteJSON,
|
||||
FetchError,
|
||||
postJSON,
|
||||
} from '../../../infrastructure/fetch-json'
|
||||
import { mapSeries } from '../../../infrastructure/promise'
|
||||
import getMeta from '../../../utils/meta'
|
||||
import { APIError } from '../components/error-alert'
|
||||
import useUserSelection from '../hooks/use-user-selection'
|
||||
import { parseEmails } from '../utils/emails'
|
||||
|
||||
export type GroupMembersContextValue = {
|
||||
users: User[]
|
||||
selectedUsers: User[]
|
||||
selectUser: (user: User) => void
|
||||
selectAllUsers: () => void
|
||||
unselectAllUsers: () => void
|
||||
unselectUser: (user: User) => void
|
||||
addMembers: (emailString: string) => void
|
||||
removeMembers: (e: any) => void
|
||||
removeMember: (user: User) => Promise<void>
|
||||
removeMemberLoading: boolean
|
||||
removeMemberError?: APIError
|
||||
inviteMemberLoading: boolean
|
||||
inviteError?: APIError
|
||||
paths: { [key: string]: string }
|
||||
}
|
||||
|
||||
export const GroupMembersContext = createContext<
|
||||
GroupMembersContextValue | undefined
|
||||
>(undefined)
|
||||
|
||||
type GroupMembersProviderProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
|
||||
const {
|
||||
users,
|
||||
setUsers,
|
||||
selectedUsers,
|
||||
selectAllUsers,
|
||||
unselectAllUsers,
|
||||
selectUser,
|
||||
unselectUser,
|
||||
} = useUserSelection(getMeta('ol-users', []))
|
||||
|
||||
const [inviteUserInflightCount, setInviteUserInflightCount] = useState(0)
|
||||
const [inviteError, setInviteError] = useState<APIError>()
|
||||
const [removeMemberInflightCount, setRemoveMemberInflightCount] = useState(0)
|
||||
const [removeMemberError, setRemoveMemberError] = useState<APIError>()
|
||||
|
||||
const groupId: string = getMeta('ol-groupId')
|
||||
|
||||
const paths = useMemo(
|
||||
() => ({
|
||||
addMember: `/manage/groups/${groupId}/invites`,
|
||||
removeMember: `/manage/groups/${groupId}/user`,
|
||||
removeInvite: `/manage/groups/${groupId}/invites`,
|
||||
exportMembers: `/manage/groups/${groupId}/members/export`,
|
||||
}),
|
||||
[groupId]
|
||||
)
|
||||
|
||||
const addMembers = useCallback(
|
||||
emailString => {
|
||||
setInviteError(undefined)
|
||||
const emails = parseEmails(emailString)
|
||||
mapSeries(emails, async email => {
|
||||
setInviteUserInflightCount(count => count + 1)
|
||||
try {
|
||||
const data = await postJSON<{ user: User }>(paths.addMember, {
|
||||
body: {
|
||||
email,
|
||||
},
|
||||
})
|
||||
if (data.user) {
|
||||
const alreadyListed = users.find(
|
||||
user => user.email === data.user.email
|
||||
)
|
||||
if (!alreadyListed) {
|
||||
setUsers(users => [...users, data.user])
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
setInviteError((error as FetchError)?.data?.error || {})
|
||||
}
|
||||
setInviteUserInflightCount(count => count - 1)
|
||||
})
|
||||
},
|
||||
[paths.addMember, users, setUsers]
|
||||
)
|
||||
|
||||
const removeMember = useCallback(
|
||||
async user => {
|
||||
let url
|
||||
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}`
|
||||
} else {
|
||||
return
|
||||
}
|
||||
setRemoveMemberInflightCount(count => count + 1)
|
||||
try {
|
||||
await deleteJSON(url, {})
|
||||
setUsers(users => users.filter(u => u !== user))
|
||||
unselectUser(user)
|
||||
} catch (error: unknown) {
|
||||
console.error(error)
|
||||
setRemoveMemberError((error as FetchError)?.data?.error || {})
|
||||
}
|
||||
setRemoveMemberInflightCount(count => count - 1)
|
||||
},
|
||||
[unselectUser, setUsers, paths.removeInvite, paths.removeMember]
|
||||
)
|
||||
|
||||
const removeMembers = useCallback(
|
||||
e => {
|
||||
e.preventDefault()
|
||||
setRemoveMemberError(undefined)
|
||||
;(async () => {
|
||||
for (const user of selectedUsers) {
|
||||
if (user?.enrollment?.managedBy) {
|
||||
continue
|
||||
}
|
||||
await removeMember(user)
|
||||
}
|
||||
})()
|
||||
},
|
||||
[selectedUsers, removeMember]
|
||||
)
|
||||
|
||||
const value = useMemo<GroupMembersContextValue>(
|
||||
() => ({
|
||||
users,
|
||||
selectedUsers,
|
||||
selectAllUsers,
|
||||
unselectAllUsers,
|
||||
selectUser,
|
||||
unselectUser,
|
||||
addMembers,
|
||||
removeMembers,
|
||||
removeMember,
|
||||
removeMemberLoading: removeMemberInflightCount > 0,
|
||||
removeMemberError,
|
||||
inviteMemberLoading: inviteUserInflightCount > 0,
|
||||
inviteError,
|
||||
paths,
|
||||
}),
|
||||
[
|
||||
users,
|
||||
selectedUsers,
|
||||
selectAllUsers,
|
||||
unselectAllUsers,
|
||||
selectUser,
|
||||
unselectUser,
|
||||
addMembers,
|
||||
removeMembers,
|
||||
removeMember,
|
||||
removeMemberInflightCount,
|
||||
removeMemberError,
|
||||
inviteUserInflightCount,
|
||||
inviteError,
|
||||
paths,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<GroupMembersContext.Provider value={value}>
|
||||
{children}
|
||||
</GroupMembersContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useGroupMembersContext() {
|
||||
const context = useContext(GroupMembersContext)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'GroupMembersContext is only available inside GroupMembersProvider'
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
|
@ -6,7 +6,6 @@ export default function useUserSelection(initialUsers: User[]) {
|
|||
const [selectedUsers, setSelectedUsers] = useState<User[]>([])
|
||||
|
||||
const selectAllUsers = () => setSelectedUsers(users)
|
||||
|
||||
const unselectAllUsers = () => setSelectedUsers([])
|
||||
|
||||
const selectUser = useCallback((user: User) => {
|
||||
|
|
|
@ -12,10 +12,16 @@ export default function MenuItemButton({
|
|||
onClick,
|
||||
className,
|
||||
afterNode,
|
||||
...buttonProps
|
||||
}: MenuItemButtonProps) {
|
||||
return (
|
||||
<li role="presentation" className={className}>
|
||||
<button className="menu-item-button" role="menuitem" onClick={onClick}>
|
||||
<button
|
||||
className="menu-item-button"
|
||||
role="menuitem"
|
||||
onClick={onClick}
|
||||
{...buttonProps}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
{afterNode}
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
// run `fn` in serie for all values, and resolve with an array of the resultss
|
||||
// inspired by https://stackoverflow.com/a/50506360/1314820
|
||||
/**
|
||||
* run `fn` in serie for all values, and resolve with an array of the results
|
||||
* inspired by https://stackoverflow.com/a/50506360/1314820
|
||||
* @template T the input array's item type
|
||||
* @template V the `fn` function's return type
|
||||
* @param {T[]} values
|
||||
* @param {(item: T) => Promise<V>} fn
|
||||
* @returns {V[]}
|
||||
*/
|
||||
export function mapSeries(values, fn) {
|
||||
return values.reduce((promiseChain, value) => {
|
||||
return promiseChain.then(chainResults =>
|
||||
fn(value).then(currentResult => [...chainResults, currentResult])
|
||||
)
|
||||
return values.reduce(async (promiseChain, value) => {
|
||||
const chainResults = await promiseChain
|
||||
const currentResult = await fn(value)
|
||||
return [...chainResults, currentResult]
|
||||
}, Promise.resolve([]))
|
||||
}
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import '../base'
|
||||
import ReactDOM from 'react-dom'
|
||||
import Members from '../../../../features/group-management/components/group-members'
|
||||
import GroupMembers from '../../../../features/group-management/components/group-members'
|
||||
import { GroupMembersProvider } from '../../../../features/group-management/context/group-members-context'
|
||||
|
||||
const element = document.getElementById('subscription-manage-group-root')
|
||||
if (element) {
|
||||
ReactDOM.render(<Members />, element)
|
||||
ReactDOM.render(
|
||||
<GroupMembersProvider>
|
||||
<GroupMembers />
|
||||
</GroupMembersProvider>,
|
||||
element
|
||||
)
|
||||
}
|
||||
|
|
|
@ -82,3 +82,7 @@
|
|||
vertical-align: text-bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.managed-users-dropdown-menu {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import GroupMembers from '../../../../../frontend/js/features/group-management/components/group-members'
|
||||
import { GroupMembersProvider } from '../../../../../frontend/js/features/group-management/context/group-members-context'
|
||||
|
||||
const JOHN_DOE = {
|
||||
_id: 'abc123def456',
|
||||
|
@ -27,13 +28,17 @@ const PATHS = {
|
|||
describe('group members, without managed users', function () {
|
||||
beforeEach(function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-users', [JOHN_DOE, BOBBY_LAPOINTE])
|
||||
win.metaAttributesCache.set('ol-groupId', GROUP_ID)
|
||||
win.metaAttributesCache.set('ol-groupName', 'My Awesome Team')
|
||||
win.metaAttributesCache.set('ol-groupSize', 10)
|
||||
win.metaAttributesCache.set('ol-users', [JOHN_DOE, BOBBY_LAPOINTE])
|
||||
})
|
||||
|
||||
cy.mount(<GroupMembers />)
|
||||
cy.mount(
|
||||
<GroupMembersProvider>
|
||||
<GroupMembers />
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
})
|
||||
|
||||
it('renders the group members page', function () {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import GroupMembers from '../../../../../../frontend/js/features/group-management/components/group-members'
|
||||
import { GroupMembersProvider } from '../../../../../../frontend/js/features/group-management/context/group-members-context'
|
||||
|
||||
const GROUP_ID = '777fff777fff'
|
||||
const JOHN_DOE = {
|
||||
|
@ -48,11 +49,14 @@ describe('group members, with managed users', function () {
|
|||
win.metaAttributesCache.set('ol-groupId', GROUP_ID)
|
||||
win.metaAttributesCache.set('ol-groupName', 'My Awesome Team')
|
||||
win.metaAttributesCache.set('ol-groupSize', 10)
|
||||
// Managed Users is active on this group
|
||||
win.metaAttributesCache.set('ol-managedUsersActive', true)
|
||||
})
|
||||
|
||||
cy.mount(<GroupMembers />)
|
||||
cy.mount(
|
||||
<GroupMembersProvider>
|
||||
<GroupMembers />
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
})
|
||||
|
||||
it('renders the group members page', function () {
|
||||
|
@ -185,7 +189,9 @@ describe('group members, with managed users', function () {
|
|||
})
|
||||
})
|
||||
|
||||
cy.get('button').contains('Remove from group').should('not.exist')
|
||||
cy.get('.page-header').within(() => {
|
||||
cy.findByRole('button', { name: 'Remove from group' }).should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
it('does not show the remove-member button if any of the selected users are managed', function () {
|
||||
|
@ -204,7 +210,9 @@ describe('group members, with managed users', function () {
|
|||
})
|
||||
})
|
||||
|
||||
cy.get('button').contains('Remove from group').should('not.exist')
|
||||
cy.get('.page-header').within(() => {
|
||||
cy.findByRole('button', { name: 'Remove from group' }).should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
it('tries to remove a user and displays the error', function () {
|
||||
|
@ -217,7 +225,9 @@ describe('group members, with managed users', function () {
|
|||
cy.get('.select-item').check()
|
||||
})
|
||||
})
|
||||
cy.get('button').contains('Remove from group').click()
|
||||
cy.get('.page-header').within(() => {
|
||||
cy.get('button').contains('Remove from group').click()
|
||||
})
|
||||
|
||||
cy.get('.alert').contains('Sorry, something went wrong')
|
||||
})
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import ManagedUserDropdownButton from '../../../../../../frontend/js/features/group-management/components/managed-users/managed-user-dropdown-button'
|
||||
import sinon from 'sinon'
|
||||
import { GroupMembersProvider } from '../../../../../../frontend/js/features/group-management/context/group-members-context'
|
||||
|
||||
describe('ManagedUserDropdownButton', function () {
|
||||
describe('with managed user', function () {
|
||||
|
@ -18,23 +19,31 @@ describe('ManagedUserDropdownButton', function () {
|
|||
}
|
||||
|
||||
beforeEach(function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-users', [user])
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<ManagedUserDropdownButton
|
||||
user={user}
|
||||
openOffboardingModalForUser={sinon.stub()}
|
||||
/>
|
||||
<GroupMembersProvider>
|
||||
<ManagedUserDropdownButton
|
||||
user={user}
|
||||
openOffboardingModalForUser={sinon.stub()}
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
})
|
||||
|
||||
it('should render the button', function () {
|
||||
cy.get(`#managed-user-dropdown-${user._id}`).should('exist')
|
||||
cy.get('#managed-user-dropdown-some\\.user\\@example\\.com').should(
|
||||
'exist'
|
||||
)
|
||||
cy.get(`.action-btn`).should('exist')
|
||||
})
|
||||
|
||||
it('should show the menu when the button is clicked', function () {
|
||||
cy.get('.action-btn').click()
|
||||
cy.get('.delete-user-action').should('exist')
|
||||
cy.get('.delete-user-action').then($el => {
|
||||
cy.findByTestId('delete-user-action').should('exist')
|
||||
cy.findByTestId('delete-user-action').then($el => {
|
||||
Cypress.dom.isVisible($el)
|
||||
})
|
||||
})
|
||||
|
@ -53,22 +62,73 @@ describe('ManagedUserDropdownButton', function () {
|
|||
}
|
||||
|
||||
beforeEach(function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-users', [user])
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<ManagedUserDropdownButton
|
||||
user={user}
|
||||
openOffboardingModalForUser={sinon.stub()}
|
||||
/>
|
||||
<GroupMembersProvider>
|
||||
<ManagedUserDropdownButton
|
||||
user={user}
|
||||
openOffboardingModalForUser={sinon.stub()}
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
})
|
||||
|
||||
it('should render the button', function () {
|
||||
cy.get(`#managed-user-dropdown-${user._id}`).should('exist')
|
||||
cy.get('#managed-user-dropdown-some\\.user\\@example\\.com').should(
|
||||
'exist'
|
||||
)
|
||||
cy.get(`.action-btn`).should('exist')
|
||||
})
|
||||
|
||||
it('should show the menu when the button is clicked', function () {
|
||||
cy.get('.action-btn').click()
|
||||
cy.findByTestId('remove-user-action').should('exist')
|
||||
cy.findByTestId('remove-user-action').then($el => {
|
||||
Cypress.dom.isVisible($el)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with group admin user', function () {
|
||||
const user = {
|
||||
_id: 'some-user',
|
||||
email: 'some.user@example.com',
|
||||
first_name: 'Some',
|
||||
last_name: 'User',
|
||||
invite: false,
|
||||
last_active_at: new Date(),
|
||||
enrollment: undefined,
|
||||
isEntityAdmin: true,
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-users', [user])
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<GroupMembersProvider>
|
||||
<ManagedUserDropdownButton
|
||||
user={user}
|
||||
openOffboardingModalForUser={sinon.stub()}
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
})
|
||||
|
||||
it('should render the button', function () {
|
||||
cy.get('#managed-user-dropdown-some\\.user\\@example\\.com').should(
|
||||
'exist'
|
||||
)
|
||||
cy.get(`.action-btn`).should('exist')
|
||||
})
|
||||
|
||||
it('should show the (empty) menu when the button is clicked', function () {
|
||||
cy.get('.action-btn').click()
|
||||
cy.get('.no-actions-available').should('exist')
|
||||
cy.findByTestId('no-actions-available').should('exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import sinon, { SinonStub } from 'sinon'
|
||||
import sinon from 'sinon'
|
||||
import ManagedUserRow from '../../../../../../frontend/js/features/group-management/components/managed-users/managed-user-row'
|
||||
import { GroupMembersProvider } from '../../../../../../frontend/js/features/group-management/context/group-members-context'
|
||||
import { User } from '../../../../../../types/group-management/user'
|
||||
|
||||
describe('ManagedUserRow', function () {
|
||||
describe('with an ordinary user', function () {
|
||||
let user: User,
|
||||
selectUser: SinonStub,
|
||||
unselectUser: SinonStub,
|
||||
selected: boolean
|
||||
let user: User
|
||||
|
||||
beforeEach(function () {
|
||||
user = {
|
||||
|
@ -20,18 +18,17 @@ describe('ManagedUserRow', function () {
|
|||
enrollment: undefined,
|
||||
isEntityAdmin: undefined,
|
||||
}
|
||||
selectUser = sinon.stub()
|
||||
unselectUser = sinon.stub()
|
||||
selected = false
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-users', [user])
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<ManagedUserRow
|
||||
user={user}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
selected={selected}
|
||||
openOffboardingModalForUser={sinon.stub()}
|
||||
/>
|
||||
<GroupMembersProvider>
|
||||
<ManagedUserRow
|
||||
user={user}
|
||||
openOffboardingModalForUser={sinon.stub()}
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -49,15 +46,14 @@ describe('ManagedUserRow', function () {
|
|||
// Managed status
|
||||
cy.get('.row').contains('Managed')
|
||||
// Dropdown button
|
||||
cy.get(`#managed-user-dropdown-${user._id}`).should('exist')
|
||||
cy.get('#managed-user-dropdown-some\\.user\\@example\\.com').should(
|
||||
'exist'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a pending invite', function () {
|
||||
let user: User,
|
||||
selectUser: SinonStub,
|
||||
unselectUser: SinonStub,
|
||||
selected: boolean
|
||||
let user: User
|
||||
|
||||
beforeEach(function () {
|
||||
user = {
|
||||
|
@ -70,18 +66,17 @@ describe('ManagedUserRow', function () {
|
|||
enrollment: undefined,
|
||||
isEntityAdmin: undefined,
|
||||
}
|
||||
selectUser = sinon.stub()
|
||||
unselectUser = sinon.stub()
|
||||
selected = false
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-users', [user])
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<ManagedUserRow
|
||||
user={user}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
selected={selected}
|
||||
openOffboardingModalForUser={sinon.stub()}
|
||||
/>
|
||||
<GroupMembersProvider>
|
||||
<ManagedUserRow
|
||||
user={user}
|
||||
openOffboardingModalForUser={sinon.stub()}
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -91,10 +86,7 @@ describe('ManagedUserRow', function () {
|
|||
})
|
||||
|
||||
describe('with a group admin', function () {
|
||||
let user: User,
|
||||
selectUser: SinonStub,
|
||||
unselectUser: SinonStub,
|
||||
selected: boolean
|
||||
let user: User
|
||||
|
||||
beforeEach(function () {
|
||||
user = {
|
||||
|
@ -107,18 +99,17 @@ describe('ManagedUserRow', function () {
|
|||
enrollment: undefined,
|
||||
isEntityAdmin: true,
|
||||
}
|
||||
selectUser = sinon.stub()
|
||||
unselectUser = sinon.stub()
|
||||
selected = false
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-users', [user])
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<ManagedUserRow
|
||||
user={user}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
selected={selected}
|
||||
openOffboardingModalForUser={sinon.stub()}
|
||||
/>
|
||||
<GroupMembersProvider>
|
||||
<ManagedUserRow
|
||||
user={user}
|
||||
openOffboardingModalForUser={sinon.stub()}
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -127,11 +118,8 @@ describe('ManagedUserRow', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('user is selected', function () {
|
||||
let user: User,
|
||||
selectUser: SinonStub,
|
||||
unselectUser: SinonStub,
|
||||
selected: boolean
|
||||
describe('selecting and unselecting user row', function () {
|
||||
let user: User
|
||||
|
||||
beforeEach(function () {
|
||||
user = {
|
||||
|
@ -144,109 +132,26 @@ describe('ManagedUserRow', function () {
|
|||
enrollment: undefined,
|
||||
isEntityAdmin: undefined,
|
||||
}
|
||||
selectUser = sinon.stub()
|
||||
unselectUser = sinon.stub()
|
||||
// User is selected
|
||||
selected = true
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-users', [user])
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
<ManagedUserRow
|
||||
user={user}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
selected={selected}
|
||||
openOffboardingModalForUser={sinon.stub()}
|
||||
/>
|
||||
<GroupMembersProvider>
|
||||
<ManagedUserRow
|
||||
user={user}
|
||||
openOffboardingModalForUser={sinon.stub()}
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
})
|
||||
|
||||
it('should render the selection box as selected', function () {
|
||||
cy.get('.select-item').should('be.checked')
|
||||
})
|
||||
})
|
||||
|
||||
describe('selecting user row', function () {
|
||||
let user: User,
|
||||
selectUser: SinonStub,
|
||||
unselectUser: SinonStub,
|
||||
selected: boolean
|
||||
|
||||
beforeEach(function () {
|
||||
user = {
|
||||
_id: 'some-user',
|
||||
email: 'some.user@example.com',
|
||||
first_name: 'Some',
|
||||
last_name: 'User',
|
||||
invite: false,
|
||||
last_active_at: new Date('2070-11-21T03:00:00'),
|
||||
enrollment: undefined,
|
||||
isEntityAdmin: undefined,
|
||||
}
|
||||
selectUser = sinon.stub()
|
||||
unselectUser = sinon.stub()
|
||||
// User is not selected
|
||||
selected = false
|
||||
|
||||
cy.mount(
|
||||
<ManagedUserRow
|
||||
user={user}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
selected={selected}
|
||||
openOffboardingModalForUser={sinon.stub()}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
it('should select the user', function () {
|
||||
it('should select and unselect the user', function () {
|
||||
cy.get('.select-item').should('not.be.checked')
|
||||
cy.get('.select-item').click()
|
||||
cy.get('.select-item').then(() => {
|
||||
expect(selectUser.called).to.equal(true)
|
||||
expect(unselectUser.called).to.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('un-selecting user row', function () {
|
||||
let user: User,
|
||||
selectUser: SinonStub,
|
||||
unselectUser: SinonStub,
|
||||
selected: boolean
|
||||
|
||||
beforeEach(function () {
|
||||
user = {
|
||||
_id: 'some-user',
|
||||
email: 'some.user@example.com',
|
||||
first_name: 'Some',
|
||||
last_name: 'User',
|
||||
invite: false,
|
||||
last_active_at: new Date('2070-11-21T03:00:00'),
|
||||
enrollment: undefined,
|
||||
isEntityAdmin: undefined,
|
||||
}
|
||||
selectUser = sinon.stub()
|
||||
unselectUser = sinon.stub()
|
||||
// User is selected
|
||||
selected = true
|
||||
|
||||
cy.mount(
|
||||
<ManagedUserRow
|
||||
user={user}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
selected={selected}
|
||||
openOffboardingModalForUser={sinon.stub()}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
it('should select the user', function () {
|
||||
cy.get('.select-item').should('be.checked')
|
||||
cy.get('.select-item').click()
|
||||
cy.get('.select-item').then(() => {
|
||||
expect(unselectUser.called).to.equal(true)
|
||||
})
|
||||
cy.get('.select-item').should('not.be.checked')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import ManagedUsersList from '../../../../../../frontend/js/features/group-management/components/managed-users/managed-users-list'
|
||||
import { User } from '../../../../../../types/group-management/user'
|
||||
import { GroupMembersProvider } from '../../../../../../frontend/js/features/group-management/context/group-members-context'
|
||||
|
||||
describe('ManagedUsersList', function () {
|
||||
const groupId = 'somegroup'
|
||||
|
||||
describe('with users', function () {
|
||||
const users: User[] = [
|
||||
const users = [
|
||||
{
|
||||
_id: 'user-one',
|
||||
email: 'sarah.brennan@example.com',
|
||||
|
@ -26,21 +27,20 @@ describe('ManagedUsersList', function () {
|
|||
isEntityAdmin: undefined,
|
||||
},
|
||||
]
|
||||
const selectedUsers: User[] = []
|
||||
const handleSelectAllClick = () => {}
|
||||
const selectUser = () => {}
|
||||
const unselectUser = () => {}
|
||||
|
||||
beforeEach(function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-users', users)
|
||||
})
|
||||
const handleSelectAllClick = () => {}
|
||||
|
||||
cy.mount(
|
||||
<ManagedUsersList
|
||||
users={users}
|
||||
selectedUsers={selectedUsers}
|
||||
handleSelectAllClick={handleSelectAllClick}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
groupId={groupId}
|
||||
/>
|
||||
<GroupMembersProvider>
|
||||
<ManagedUsersList
|
||||
handleSelectAllClick={handleSelectAllClick}
|
||||
groupId={groupId}
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -70,23 +70,21 @@ describe('ManagedUsersList', function () {
|
|||
cy.get('.managed-users-list').contains(users[1].last_name)
|
||||
})
|
||||
})
|
||||
|
||||
describe('empty user list', function () {
|
||||
const users: User[] = []
|
||||
const selectedUsers: User[] = []
|
||||
const handleSelectAllClick = () => {}
|
||||
const selectUser = () => {}
|
||||
const unselectUser = () => {}
|
||||
|
||||
beforeEach(function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set('ol-users', [])
|
||||
})
|
||||
cy.mount(
|
||||
<ManagedUsersList
|
||||
users={users}
|
||||
selectedUsers={selectedUsers}
|
||||
handleSelectAllClick={handleSelectAllClick}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
groupId={groupId}
|
||||
/>
|
||||
<GroupMembersProvider>
|
||||
<ManagedUsersList
|
||||
handleSelectAllClick={handleSelectAllClick}
|
||||
groupId={groupId}
|
||||
/>
|
||||
</GroupMembersProvider>
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
@ -10,6 +10,6 @@ export type User = {
|
|||
last_name: string
|
||||
invite: boolean
|
||||
last_active_at: Date
|
||||
enrollment: UserEnrollment | undefined
|
||||
isEntityAdmin: boolean | undefined
|
||||
enrollment?: UserEnrollment
|
||||
isEntityAdmin?: boolean
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue