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:
Alexandre Bourdin 2023-08-22 15:30:34 +02:00 committed by Copybot
parent d73f77e88e
commit a4a5a08c31
17 changed files with 445 additions and 348 deletions

View file

@ -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">

View file

@ -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')}&hellip;
</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')}&hellip;
</Button>
) : (
<Button bsStyle="primary" onClick={addMembers}>
<Button bsStyle="primary" onClick={onAddMembersSubmit}>
{t('add')}
</Button>
)}

View file

@ -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>

View file

@ -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}`}

View file

@ -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}
/>
))}

View file

@ -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
}

View file

@ -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) => {

View file

@ -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}

View file

@ -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([]))
}

View file

@ -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
)
}

View file

@ -82,3 +82,7 @@
vertical-align: text-bottom;
}
}
.managed-users-dropdown-menu {
min-width: 200px;
}

View file

@ -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 () {

View file

@ -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')
})

View file

@ -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')
})
})
})

View file

@ -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')
})
})
})

View file

@ -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>
)
})

View file

@ -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
}