Merge pull request #15348 from overleaf/ab-decoupling-refactor-members-table

[web] Modular members management table refactoring

GitOrigin-RevId: 9a3a00a32970e78e5b43b3a68621a627c490c728
This commit is contained in:
Alexandre Bourdin
2023-10-27 15:11:34 +02:00
committed by Copybot
parent 0ed80e9f44
commit 1d43dcc5c1
28 changed files with 1221 additions and 555 deletions

View File

@@ -1074,6 +1074,7 @@
"source": "",
"spell_check": "",
"sso": "",
"sso_active": "",
"sso_config_prop_help_certificate": "",
"sso_config_prop_help_first_name": "",
"sso_config_prop_help_last_name": "",
@@ -1092,9 +1093,8 @@
"sso_is_enabled_explanation_2": "",
"sso_link_error": "",
"sso_link_invite_has_been_sent_to_email": "",
"sso_linked": "",
"sso_logs": "",
"sso_unlinked": "",
"sso_not_active": "",
"start_a_free_trial": "",
"start_by_adding_your_email": "",
"start_free_trial": "",

View File

@@ -1,89 +0,0 @@
import { useCallback } from 'react'
import { Col, Row } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import Tooltip from '../../../shared/components/tooltip'
import { useGroupMembersContext } from '../context/group-members-context'
import GroupMemberRow from './group-member-row'
export default function GroupMembersList() {
const { t } = useTranslation()
const {
selectedUsers,
users,
selectUser,
unselectUser,
selectAllUsers,
unselectAllUsers,
} = useGroupMembersContext()
const handleSelectAllClick = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.checked) {
selectAllUsers()
} else {
unselectAllUsers()
}
},
[selectAllUsers, unselectAllUsers]
)
return (
<ul className="list-unstyled structured-list">
<li className="container-fluid">
<Row>
<Col xs={4}>
<label htmlFor="select-all" className="sr-only">
{t('select_all')}
</label>
<input
className="select-all"
id="select-all"
type="checkbox"
onChange={handleSelectAllClick}
checked={selectedUsers.length === users.length}
/>
<span className="header">{t('email')}</span>
</Col>
<Col xs={4}>
<span className="header">{t('name')}</span>
</Col>
<Col xs={2}>
<Tooltip
id="last-active-tooltip"
description={t('last_active_description')}
overlayProps={{
placement: 'left',
}}
>
<span className="header">
{t('last_active')}
<sup>(?)</sup>
</span>
</Tooltip>
</Col>
<Col xs={2}>
<span className="header">{t('accepted_invite')}</span>
</Col>
</Row>
</li>
{users.length === 0 && (
<li>
<Row>
<Col md={12} className="text-centered">
<small>{t('no_members')}</small>
</Col>
</Row>
</li>
)}
{users.map((user: any) => (
<GroupMemberRow
key={user.email}
user={user}
selectUser={selectUser}
unselectUser={unselectUser}
selected={selectedUsers.includes(user)}
/>
))}
</ul>
)
}

View File

@@ -6,8 +6,7 @@ import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n'
import getMeta from '../../../utils/meta'
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'
import MembersList from './members-table/members-list'
export default function GroupMembers() {
const { isReady } = useWaitForI18n()
@@ -28,7 +27,6 @@ export default function GroupMembers() {
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 handleEmailsChange = useCallback(
e => {
@@ -91,11 +89,7 @@ export default function GroupMembers() {
</div>
<div className="row-spaced-small">
<ErrorAlert error={removeMemberError} />
{managedUsersActive ? (
<ManagedUsersList groupId={groupId} />
) : (
<GroupMembersList />
)}
<MembersList groupId={groupId} />
</div>
<hr />
{users.length < groupSize && (

View File

@@ -1,17 +1,13 @@
import { useCallback, useState } from 'react'
import { Button, Col, Form, FormControl, Row } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import {
deleteJSON,
FetchError,
postJSON,
} from '../../../infrastructure/fetch-json'
import { deleteJSON, FetchError, postJSON } from '@/infrastructure/fetch-json'
import MaterialIcon from '../../../shared/components/material-icon'
import Tooltip from '../../../shared/components/tooltip'
import getMeta from '../../../utils/meta'
import { parseEmails } from '../utils/emails'
import ErrorAlert, { APIError } from './error-alert'
import GroupMemberRow from './group-member-row'
import UserRow from './user-row'
import useUserSelection from '../hooks/use-user-selection'
import { User } from '../../../../../types/group-management/user'
import { debugConsole } from '@/utils/debugging'
@@ -216,7 +212,7 @@ export function ManagersTable({
</li>
)}
{users.map(user => (
<GroupMemberRow
<UserRow
key={user.email}
user={user}
selectUser={selectUser}

View File

@@ -8,12 +8,9 @@ import {
import { useTranslation } from 'react-i18next'
import { Dropdown, MenuItem } from 'react-bootstrap'
import { User } from '../../../../../../types/group-management/user'
import useAsync from '../../../../shared/hooks/use-async'
import {
type FetchError,
postJSON,
} from '../../../../infrastructure/fetch-json'
import Icon from '../../../../shared/components/icon'
import useAsync from '@/shared/hooks/use-async'
import { type FetchError, postJSON } from '@/infrastructure/fetch-json'
import Icon from '@/shared/components/icon'
import { ManagedUserAlert } from '../../utils/types'
import { useGroupMembersContext } from '../../context/group-members-context'
import getMeta from '@/utils/meta'
@@ -28,7 +25,7 @@ type ManagedUserDropdownButtonProps = {
setManagedUserAlert: Dispatch<SetStateAction<ManagedUserAlert>>
}
export default function ManagedUserDropdownButton({
export default function DropdownButton({
user,
openOffboardingModalForUser,
groupId,

View File

@@ -9,7 +9,7 @@ type ManagedUsersListAlertProps = {
onDismiss: () => void
}
export default function ManagedUsersListAlert({
export default function ListAlert({
variant,
invitedUserEmail,
onDismiss,

View File

@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next'
import { User } from '../../../../../../types/group-management/user'
import MaterialIcon from '../../../../shared/components/material-icon'
import MaterialIcon from '@/shared/components/material-icon'
type ManagedUserStatusProps = {
user: User

View File

@@ -7,8 +7,8 @@ import Tooltip from '../../../../shared/components/tooltip'
import type { ManagedUserAlert } from '../../utils/types'
import ManagedUserStatus from './managed-user-status'
import SSOStatus from './sso-status'
import ManagedUserDropdownButton from './managed-user-dropdown-button'
import ManagedUsersSelectUserCheckbox from './managed-users-select-user-checkbox'
import DropdownButton from './dropdown-button'
import SelectUserCheckbox from './select-user-checkbox'
import getMeta from '@/utils/meta'
type ManagedUserRowProps = {
@@ -18,13 +18,14 @@ type ManagedUserRowProps = {
setManagedUserAlert: Dispatch<SetStateAction<ManagedUserAlert>>
}
export default function ManagedUserRow({
export default function MemberRow({
user,
openOffboardingModalForUser,
setManagedUserAlert,
groupId,
}: ManagedUserRowProps) {
const { t } = useTranslation()
const managedUsersActive: any = getMeta('ol-managedUsersActive')
const groupSSOActive = getMeta('ol-groupSSOActive')
return (
@@ -32,8 +33,8 @@ export default function ManagedUserRow({
key={`user-${user.email}`}
className={`managed-user-row ${user.invite ? 'text-muted' : ''}`}
>
<ManagedUsersSelectUserCheckbox user={user} />
<td className={groupSSOActive ? 'cell-email-with-sso-col' : 'cell-email'}>
<SelectUserCheckbox user={user} />
<td className="cell-email">
<span>
{user.email}
{user.invite ? (
@@ -83,13 +84,15 @@ export default function ManagedUserRow({
</div>
</td>
)}
<td className="cell-managed">
<div className="managed-user-security">
<ManagedUserStatus user={user} />
</div>
</td>
{managedUsersActive && (
<td className="cell-managed">
<div className="managed-user-security">
<ManagedUserStatus user={user} />
</div>
</td>
)}
<td className="cell-dropdown">
<ManagedUserDropdownButton
<DropdownButton
user={user}
openOffboardingModalForUser={openOffboardingModalForUser}
setManagedUserAlert={setManagedUserAlert}

View File

@@ -2,20 +2,21 @@ import { useState } from 'react'
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 Tooltip from '@/shared/components/tooltip'
import { useGroupMembersContext } from '../../context/group-members-context'
import type { ManagedUserAlert } from '../../utils/types'
import ManagedUserRow from './managed-user-row'
import MemberRow from './member-row'
import OffboardManagedUserModal from './offboard-managed-user-modal'
import ManagedUsersListAlert from './managed-users-list-alert'
import ManagedUsersSelectAllCheckbox from './managed-users-select-all-checkbox'
import ListAlert from './list-alert'
import SelectAllCheckbox from './select-all-checkbox'
import classNames from 'classnames'
import getMeta from '@/utils/meta'
type ManagedUsersListProps = {
groupId: string
}
export default function ManagedUsersList({ groupId }: ManagedUsersListProps) {
export default function MembersList({ groupId }: ManagedUsersListProps) {
const { t } = useTranslation()
const [userToOffboard, setUserToOffboard] = useState<User | undefined>(
undefined
@@ -23,32 +24,37 @@ export default function ManagedUsersList({ groupId }: ManagedUsersListProps) {
const [managedUserAlert, setManagedUserAlert] =
useState<ManagedUserAlert>(undefined)
const { users } = useGroupMembersContext()
const managedUsersActive: any = getMeta('ol-managedUsersActive')
const groupSSOActive = getMeta('ol-groupSSOActive')
return (
<div>
{managedUserAlert && (
<ManagedUsersListAlert
{managedUsersActive && managedUserAlert && (
<ListAlert
variant={managedUserAlert.variant}
invitedUserEmail={managedUserAlert.email}
onDismiss={() => setManagedUserAlert(undefined)}
/>
)}
<ul className="list-unstyled structured-list managed-users-list">
<ul
className={classNames(
'list-unstyled',
'structured-list',
'managed-users-list',
{
'managed-users-active': managedUsersActive,
'group-sso-active': groupSSOActive,
}
)}
>
<li className="container-fluid">
<Row id="managed-users-list-headers">
<Col xs={12}>
<table className="managed-users-table">
<thead>
<tr>
<ManagedUsersSelectAllCheckbox />
<td
className={
groupSSOActive
? 'cell-email-with-sso-col'
: 'cell-email'
}
>
<SelectAllCheckbox />
<td className="cell-email">
<span className="header">{t('email')}</span>
</td>
<td className="cell-name">
@@ -73,9 +79,11 @@ export default function ManagedUsersList({ groupId }: ManagedUsersListProps) {
<span className="header">{t('security')}</span>
</td>
)}
<td className="cell-managed">
<span className="header">{t('managed')}</span>
</td>
{managedUsersActive && (
<td className="cell-managed">
<span className="header">{t('managed')}</span>
</td>
)}
<td />
</tr>
</thead>
@@ -88,7 +96,7 @@ export default function ManagedUsersList({ groupId }: ManagedUsersListProps) {
</tr>
)}
{users.map((user: any) => (
<ManagedUserRow
<MemberRow
key={user.email}
user={user}
openOffboardingModalForUser={setUserToOffboard}

View File

@@ -8,13 +8,13 @@ import {
FormGroup,
Modal,
} from 'react-bootstrap'
import AccessibleModal from '../../../../shared/components/accessible-modal'
import Icon from '../../../../shared/components/icon'
import AccessibleModal from '@/shared/components/accessible-modal'
import Icon from '@/shared/components/icon'
import { useState } from 'react'
import useAsync from '../../../../shared/hooks/use-async'
import useAsync from '@/shared/hooks/use-async'
import { useTranslation } from 'react-i18next'
import { useLocation } from '../../../../shared/hooks/use-location'
import { FetchError, postJSON } from '../../../../infrastructure/fetch-json'
import { useLocation } from '@/shared/hooks/use-location'
import { FetchError, postJSON } from '@/infrastructure/fetch-json'
import { debugConsole } from '@/utils/debugging'
type OffboardManagedUserModalProps = {

View File

@@ -1,11 +1,9 @@
import { useCallback } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useGroupMembersContext } from '../../context/group-members-context'
import getMeta from '@/utils/meta'
export default function ManagedUsersSelectAllCheckbox() {
export default function SelectAllCheckbox() {
const { t } = useTranslation()
const groupSSOActive = getMeta('ol-groupSSOActive')
const { selectedUsers, users, selectAllNonManagedUsers, unselectAllUsers } =
useGroupMembersContext()
@@ -30,11 +28,7 @@ export default function ManagedUsersSelectAllCheckbox() {
}
return (
<td
className={
groupSSOActive ? 'cell-checkbox-with-sso-col ' : 'cell-checkbox'
}
>
<td className="cell-checkbox">
<label htmlFor="select-all" className="sr-only">
{t('select_all')}
</label>

View File

@@ -2,17 +2,15 @@ import { useTranslation } from 'react-i18next'
import type { User } from '../../../../../../types/group-management/user'
import { useGroupMembersContext } from '../../context/group-members-context'
import { useCallback } from 'react'
import getMeta from '@/utils/meta'
type ManagedUsersSelectUserCheckboxProps = {
user: User
}
export default function ManagedUsersSelectUserCheckbox({
export default function SelectUserCheckbox({
user,
}: ManagedUsersSelectUserCheckboxProps) {
const { t } = useTranslation()
const groupSSOActive = getMeta('ol-groupSSOActive')
const { users, selectedUsers, selectUser, unselectUser } =
useGroupMembersContext()
@@ -40,11 +38,7 @@ export default function ManagedUsersSelectUserCheckbox({
const selected = selectedUsers.includes(user)
return (
<td
className={
groupSSOActive ? 'cell-checkbox-with-sso-col ' : 'cell-checkbox'
}
>
<td className="cell-checkbox">
{/* the next check will hide the `checkbox` but still show the `td` */}
{user.enrollment?.managedBy ? null : (
<>

View File

@@ -19,13 +19,13 @@ export default function SSOStatus({ user }: SSOStatusProps) {
)
const acceptedSSO = (
<span className="security-state-managed">
<MaterialIcon type="check" accessibilityLabel={t('sso_linked')} />
<MaterialIcon type="check" accessibilityLabel={t('sso_active')} />
&nbsp; {t('sso')}
</span>
)
const notAcceptedSSO = (
<span className="security-state-not-managed">
<MaterialIcon type="close" accessibilityLabel={t('sso_unlinked')} />
<MaterialIcon type="close" accessibilityLabel={t('sso_not_active')} />
&nbsp; {t('sso')}
</span>
)

View File

@@ -11,7 +11,7 @@ type GroupMemberRowProps = {
selected: boolean
}
export default function GroupMemberRow({
export default function UserRow({
user,
selectUser,
unselectUser,

View File

@@ -7,13 +7,9 @@ import {
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 { 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'

View File

@@ -103,75 +103,39 @@
}
}
.cell-checkbox {
width: 5%;
}
.cell-checkbox-with-sso-col {
width: 2.5%;
}
.cell-email {
width: 45%;
}
.cell-email-with-sso-col {
width: 37%;
}
.cell-name {
width: 15%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cell-last-active {
width: 15%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cell-security {
width: 10%;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cell-managed {
width: 15%;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cell-dropdown {
width: 5%;
min-width: 25px;
}
@media (min-width: @screen-xs) {
.cell-checkbox {
width: 5%;
}
.cell-checkbox-with-sso-col {
width: 2.5%;
.managed-users-active.group-sso-active & {
width: 3%;
}
}
.cell-email {
width: 34%;
}
.cell-email-with-sso-col {
width: 29%;
width: 50%;
.managed-users-active & {
width: 35%;
}
.group-sso-active & {
width: 37%;
}
.managed-users-active.group-sso-active & {
width: 29%;
}
}
.cell-name {
width: 20%;
.managed-users-active.group-sso-active & {
width: 18%;
}
}
.cell-last-active {
width: 20%;
.managed-users-active.group-sso-active & {
width: 16%;
}
}
.cell-security {
@@ -191,20 +155,28 @@
@media (min-width: @screen-lg) {
.cell-checkbox {
width: 5%;
.managed-users-active.group-sso-active & {
width: 3%;
}
}
.cell-checkbox-with-sso-col {
width: 2.5%;
}
.cell-email {
width: 43%;
}
.cell-email-with-sso-col {
width: 37%;
width: 55%;
.managed-users-active & {
width: 42%;
}
.group-sso-active & {
width: 45%;
}
.managed-users-active.group-sso-active & {
width: 36%;
}
}
.cell-name {
width: 20%;
.managed-users-active.group-sso-active & {
width: 18%;
}
}
.cell-last-active {
@@ -216,7 +188,7 @@
}
.cell-managed {
width: 12%;
width: 13%;
}
.cell-dropdown {

View File

@@ -1644,6 +1644,7 @@
"spread_the_word_and_fill_bar": "Spread the word and fill this bar up",
"sso": "SSO",
"sso_account_already_linked": "Account already linked to another __appName__ user",
"sso_active": "SSO active",
"sso_config_prop_help_certificate": "Base64 encoded certificate without whitespace",
"sso_config_prop_help_first_name": "Property in SAML assertion to use for first name",
"sso_config_prop_help_last_name": "Property in SAML assertion to use for last name",
@@ -1664,10 +1665,9 @@
"sso_is_enabled_explanation_2": "If there are any problems with the configuration, only you (as the group administrator) will be able to disable SSO.",
"sso_link_error": "Error linking account",
"sso_link_invite_has_been_sent_to_email": "An SSO invite reminder has been sent to <0>__email__</0>",
"sso_linked": "SSO linked",
"sso_logs": "SSO Logs",
"sso_not_active": "SSO not active",
"sso_not_linked": "You have not linked your account to __provider__. Please log in to your account another way and link your __provider__ account via your account settings.",
"sso_unlinked": "SSO unlinked",
"sso_user_denied_access": "Cannot log in because __appName__ was not granted access to your __provider__ account. Please try again.",
"standard": "Standard",
"start_a_free_trial": "Start a free trial",

View File

@@ -1,4 +1,4 @@
import GroupManagers from '../../../../../frontend/js/features/group-management/components/group-managers'
import GroupManagers from '@/features/group-management/components/group-managers'
const JOHN_DOE = {
_id: 'abc123def456',

View File

@@ -1,22 +1,6 @@
import GroupMembers from '../../../../../frontend/js/features/group-management/components/group-members'
import { GroupMembersProvider } from '../../../../../frontend/js/features/group-management/context/group-members-context'
import GroupMembers from '@/features/group-management/components/group-members'
import { GroupMembersProvider } from '@/features/group-management/context/group-members-context'
const JOHN_DOE = {
_id: 'abc123def456',
first_name: 'John',
last_name: 'Doe',
email: 'john.doe@test.com',
last_active_at: new Date('2023-01-15'),
invite: true,
}
const BOBBY_LAPOINTE = {
_id: 'bcd234efa567',
first_name: 'Bobby',
last_name: 'Lapointe',
email: 'bobby.lapointe@test.com',
last_active_at: new Date('2023-01-02'),
invite: false,
}
const GROUP_ID = '777fff777fff'
const PATHS = {
addMember: `/manage/groups/${GROUP_ID}/invites`,
@@ -25,139 +9,436 @@ const PATHS = {
exportMembers: `/manage/groups/${GROUP_ID}/members/export`,
}
describe('group members, without managed users', function () {
beforeEach(function () {
cy.window().then(win => {
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])
})
describe('GroupMembers', function () {
function mountGroupMembersProvider() {
cy.mount(
<GroupMembersProvider>
<GroupMembers />
</GroupMembersProvider>
)
})
}
it('renders the group members page', function () {
cy.get('h1').contains('My Awesome Team')
cy.get('small').contains('You have added 2 of 10 available members')
describe('with Managed Users and Group SSO disabled', function () {
const JOHN_DOE = {
_id: 'abc123def456',
first_name: 'John',
last_name: 'Doe',
email: 'john.doe@test.com',
last_active_at: new Date('2023-01-15'),
invite: true,
}
const BOBBY_LAPOINTE = {
_id: 'bcd234efa567',
first_name: 'Bobby',
last_name: 'Lapointe',
email: 'bobby.lapointe@test.com',
last_active_at: new Date('2023-01-02'),
invite: false,
}
cy.get('ul').within(() => {
cy.get('li:nth-child(2)').within(() => {
cy.contains('john.doe@test.com')
cy.contains('John Doe')
cy.contains('15th Jan 2023')
cy.get(`[aria-label="Invite not yet accepted"]`)
beforeEach(function () {
cy.window().then(win => {
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.get('li:nth-child(3)').within(() => {
cy.contains('bobby.lapointe@test.com')
cy.contains('Bobby Lapointe')
cy.contains('2nd Jan 2023')
cy.get(`[aria-label="Accepted invite"]`)
cy.mount(
<GroupMembersProvider>
<GroupMembers />
</GroupMembersProvider>
)
})
it('renders the group members page', function () {
cy.get('h1').contains('My Awesome Team')
cy.get('small').contains('You have added 2 of 10 available members')
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.contains('john.doe@test.com')
cy.contains('John Doe')
cy.contains('15th Jan 2023')
cy.get('.badge-new-comment').contains('Pending invite')
})
cy.get('tr:nth-child(2)').within(() => {
cy.contains('bobby.lapointe@test.com')
cy.contains('Bobby Lapointe')
cy.contains('2nd Jan 2023')
cy.get('.badge-new-comment').should('not.exist')
})
})
})
it('sends an invite', function () {
cy.intercept('POST', PATHS.addMember, {
statusCode: 201,
body: {
user: {
email: 'someone.else@test.com',
invite: true,
},
},
})
cy.get('.form-control').type('someone.else@test.com')
cy.get('.add-more-members-form button').click()
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(3)').within(() => {
cy.contains('someone.else@test.com')
cy.contains('N/A')
cy.get('.badge-new-comment').contains('Pending invite')
})
})
})
it('tries to send an invite and displays the error', function () {
cy.intercept('POST', PATHS.addMember, {
statusCode: 500,
body: {
error: {
message: 'User already added',
},
},
})
cy.get('.form-control').type('someone.else@test.com')
cy.get('.add-more-members-form button').click()
cy.get('.alert').contains('Error: User already added')
})
it('checks the select all checkbox', function () {
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.get('.select-item').should('not.be.checked')
})
cy.get('tr:nth-child(2)').within(() => {
cy.get('.select-item').should('not.be.checked')
})
})
cy.get('.select-all').click()
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.get('.select-item').should('be.checked')
})
cy.get('tr:nth-child(2)').within(() => {
cy.get('.select-item').should('be.checked')
})
})
})
it('remove a member', function () {
cy.intercept('DELETE', `${PATHS.removeMember}/abc123def456`, {
statusCode: 200,
})
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.get('.select-item').check()
})
})
cy.get('button').contains('Remove from group').click()
cy.get('small').contains('You have added 1 of 10 available members')
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.contains('bobby.lapointe@test.com')
cy.contains('Bobby Lapointe')
cy.contains('2nd Jan 2023')
cy.contains('Pending invite').should('not.exist')
})
})
})
it('tries to remove a user and displays the error', function () {
cy.intercept('DELETE', `${PATHS.removeMember}/abc123def456`, {
statusCode: 500,
})
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.get('.select-item').check()
})
})
cy.get('button').contains('Remove from group').click()
cy.get('.alert').contains('Sorry, something went wrong')
})
})
it('sends an invite', function () {
cy.intercept('POST', PATHS.addMember, {
statusCode: 201,
body: {
user: {
email: 'someone.else@test.com',
invite: true,
describe('with Managed Users enabled', function () {
const JOHN_DOE = {
_id: 'abc123def456',
first_name: 'John',
last_name: 'Doe',
email: 'john.doe@test.com',
last_active_at: new Date('2023-01-15'),
invite: true,
}
const BOBBY_LAPOINTE = {
_id: 'bcd234efa567',
first_name: 'Bobby',
last_name: 'Lapointe',
email: 'bobby.lapointe@test.com',
last_active_at: new Date('2023-01-02'),
invite: false,
}
const CLAIRE_JENNINGS = {
_id: 'defabc231453',
first_name: 'Claire',
last_name: 'Jennings',
email: 'claire.jennings@test.com',
last_active_at: new Date('2023-01-03'),
invite: false,
enrollment: {
managedBy: GROUP_ID,
enrolledAt: new Date('2023-01-03'),
sso: {
providerId: '123',
externalId: '123',
},
},
}
beforeEach(function () {
cy.window().then(win => {
win.metaAttributesCache = new Map()
win.metaAttributesCache.set('ol-users', [
JOHN_DOE,
BOBBY_LAPOINTE,
CLAIRE_JENNINGS,
])
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-managedUsersActive', true)
})
mountGroupMembersProvider()
})
cy.get('.form-control').type('someone.else@test.com')
cy.get('button').click()
it('renders the group members page', function () {
cy.get('h1').contains('My Awesome Team')
cy.get('small').contains('You have added 3 of 10 available members')
cy.get('ul').within(() => {
cy.get('li:nth-child(4)').within(() => {
cy.contains('someone.else@test.com')
cy.contains('N/A')
cy.get(`[aria-label="Invite not yet accepted"]`)
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.contains('john.doe@test.com')
cy.contains('John Doe')
cy.contains('15th Jan 2023')
cy.get('.sr-only').contains('Pending invite')
cy.get('.badge-new-comment').contains('Pending invite')
cy.get(`.security-state-invite-pending`).should('exist')
cy.get('.badge-new-comment').contains('Pending invite')
cy.get(`.security-state-invite-pending`).should('exist')
})
cy.get('tr:nth-child(2)').within(() => {
cy.contains('bobby.lapointe@test.com')
cy.contains('Bobby Lapointe')
cy.contains('2nd Jan 2023')
cy.get('.badge-new-comment').should('not.exist')
cy.get('.sr-only').contains('Not managed')
})
cy.get('tr:nth-child(3)').within(() => {
cy.contains('claire.jennings@test.com')
cy.contains('Claire Jennings')
cy.contains('3rd Jan 2023')
cy.get('.badge-new-comment').should('not.exist')
cy.get('.sr-only').contains('Managed')
})
})
})
it('sends an invite', function () {
cy.intercept('POST', PATHS.addMember, {
statusCode: 201,
body: {
user: {
email: 'someone.else@test.com',
invite: true,
},
},
})
cy.get('.form-control').type('someone.else@test.com')
cy.get('.add-more-members-form button').click()
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(4)').within(() => {
cy.contains('someone.else@test.com')
cy.contains('N/A')
cy.get('.sr-only').contains('Pending invite')
cy.get('.badge-new-comment').contains('Pending invite')
cy.get(`.security-state-invite-pending`).should('exist')
})
})
})
it('tries to send an invite and displays the error', function () {
cy.intercept('POST', PATHS.addMember, {
statusCode: 500,
body: {
error: {
message: 'User already added',
},
},
})
cy.get('.form-control').type('someone.else@test.com')
cy.get('.add-more-members-form button').click()
cy.get('.alert').contains('Error: User already added')
})
it('checks the select all checkbox', function () {
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.get('.select-item').should('not.be.checked')
})
cy.get('tr:nth-child(2)').within(() => {
cy.get('.select-item').should('not.be.checked')
})
})
cy.get('.select-all').click()
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.get('.select-item').should('be.checked')
})
cy.get('tr:nth-child(2)').within(() => {
cy.get('.select-item').should('be.checked')
})
})
cy.get('button').contains('Remove from group').click()
})
it('remove a member', function () {
cy.intercept('DELETE', `${PATHS.removeMember}/abc123def456`, {
statusCode: 200,
})
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.get('.select-item').check()
})
})
cy.get('button').contains('Remove from group').click()
cy.get('small').contains('You have added 2 of 10 available members')
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.contains('bobby.lapointe@test.com')
cy.contains('Bobby Lapointe')
cy.contains('2nd Jan 2023')
})
})
})
it('cannot remove a managed member', function () {
cy.intercept('DELETE', `${PATHS.removeMember}/abc123def456`, {
statusCode: 200,
})
cy.get('ul.managed-users-list table > tbody').within(() => {
// no checkbox should be shown for 'Claire Jennings', a managed user
cy.get('tr:nth-child(3)').within(() => {
cy.get('.select-item').should('not.exist')
})
})
})
it('tries to remove a user and displays the error', function () {
cy.intercept('DELETE', `${PATHS.removeMember}/abc123def456`, {
statusCode: 500,
})
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.get('.select-item').check()
})
})
cy.get('.page-header').within(() => {
cy.get('button').contains('Remove from group').click()
})
cy.get('.alert').contains('Sorry, something went wrong')
})
})
it('tries to send an invite and displays the error', function () {
cy.intercept('POST', PATHS.addMember, {
statusCode: 500,
body: {
error: {
message: 'User already added',
describe('with Group SSO enabled', function () {
const JOHN_DOE = {
_id: 'abc123def456',
first_name: 'John',
last_name: 'Doe',
email: 'john.doe@test.com',
last_active_at: new Date('2023-01-15'),
invite: true,
}
const BOBBY_LAPOINTE = {
_id: 'bcd234efa567',
first_name: 'Bobby',
last_name: 'Lapointe',
email: 'bobby.lapointe@test.com',
last_active_at: new Date('2023-01-02'),
invite: false,
}
const CLAIRE_JENNINGS = {
_id: 'defabc231453',
first_name: 'Claire',
last_name: 'Jennings',
email: 'claire.jennings@test.com',
last_active_at: new Date('2023-01-03'),
invite: false,
enrollment: {
managedBy: GROUP_ID,
enrolledAt: new Date('2023-01-03'),
sso: {
providerId: '123',
externalId: '123',
},
},
}
beforeEach(function () {
cy.window().then(win => {
win.metaAttributesCache = new Map()
win.metaAttributesCache.set('ol-users', [
JOHN_DOE,
BOBBY_LAPOINTE,
CLAIRE_JENNINGS,
])
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-managedUsersActive', false)
win.metaAttributesCache.set('ol-groupSSOActive', true)
})
mountGroupMembersProvider()
})
cy.get('.form-control').type('someone.else@test.com')
cy.get('button').click()
cy.get('.alert').contains('Error: User already added')
})
it('should display the Security column', function () {
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(2)').within(() => {
cy.contains('bobby.lapointe@test.com')
cy.get('.sr-only').contains('SSO not active')
})
it('checks the select all checkbox', function () {
cy.get('ul').within(() => {
cy.get('li:nth-child(2)').within(() => {
cy.get('.select-item').should('not.be.checked')
})
cy.get('li:nth-child(3)').within(() => {
cy.get('.select-item').should('not.be.checked')
cy.get('tr:nth-child(3)').within(() => {
cy.contains('claire.jennings@test.com')
cy.get('.sr-only').contains('SSO active')
})
})
})
cy.get('.select-all').click()
cy.get('ul').within(() => {
cy.get('li:nth-child(2)').within(() => {
cy.get('.select-item').should('be.checked')
})
cy.get('li:nth-child(3)').within(() => {
cy.get('.select-item').should('be.checked')
})
})
})
it('remove a member', function () {
cy.intercept('DELETE', `${PATHS.removeMember}/abc123def456`, {
statusCode: 200,
})
cy.get('ul').within(() => {
cy.get('li:nth-child(2)').within(() => {
cy.get('.select-item').check()
})
})
cy.get('button').contains('Remove from group').click()
cy.get('small').contains('You have added 1 of 10 available members')
cy.get('ul').within(() => {
cy.get('li:nth-child(2)').within(() => {
cy.contains('bobby.lapointe@test.com')
cy.contains('Bobby Lapointe')
cy.contains('2nd Jan 2023')
cy.get(`[aria-label="Accepted invite"]`)
})
})
})
it('tries to remove a user and displays the error', function () {
cy.intercept('DELETE', `${PATHS.removeMember}/abc123def456`, {
statusCode: 500,
})
cy.get('ul').within(() => {
cy.get('li:nth-child(2)').within(() => {
cy.get('.select-item').check()
})
})
cy.get('button').contains('Remove from group').click()
cy.get('.alert').contains('Sorry, something went wrong')
})
})

View File

@@ -1,4 +1,4 @@
import InstitutionManagers from '../../../../../frontend/js/features/group-management/components/institution-managers'
import InstitutionManagers from '@/features/group-management/components/institution-managers'
const JOHN_DOE = {
_id: 'abc123def456',

View File

@@ -1,5 +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'
import GroupMembers from '@/features/group-management/components/group-members'
import { GroupMembersProvider } from '@/features/group-management/context/group-members-context'
const GROUP_ID = '777fff777fff'
const JOHN_DOE = {
@@ -241,16 +241,17 @@ describe('Group members when group SSO is enabled', function () {
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(2)').within(() => {
cy.contains('bobby.lapointe@test.com')
cy.get('.sr-only').contains('SSO unlinked').should('not.exist')
cy.get('.sr-only').contains('SSO not active').should('not.exist')
})
cy.get('tr:nth-child(3)').within(() => {
cy.contains('claire.jennings@test.com')
cy.get('.sr-only').contains('SSO linked').should('not.exist')
cy.get('.sr-only').contains('SSO active').should('not.exist')
})
})
})
it('should display SSO Column when group sso is not enabled', function () {
it('should display SSO Column when Group SSO is enabled', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-groupSSOActive', true)
})
@@ -258,12 +259,12 @@ describe('Group members when group SSO is enabled', function () {
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(2)').within(() => {
cy.contains('bobby.lapointe@test.com')
cy.get('.sr-only').contains('SSO unlinked')
cy.get('.sr-only').contains('SSO not active')
})
cy.get('tr:nth-child(3)').within(() => {
cy.contains('claire.jennings@test.com')
cy.get('.sr-only').contains('SSO linked')
cy.get('.sr-only').contains('SSO active')
})
})
})

View File

@@ -1,167 +0,0 @@
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 () {
const subscriptionId = '123abc'
describe('with an ordinary user', function () {
let user: User
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,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<ManagedUserRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
it('renders the row', function () {
cy.get('tr').should('exist')
// Checkbox
cy.get('.select-item').should('not.be.checked')
// Email
cy.get('tr').contains(user.email)
// Name
cy.get('tr').contains(user.first_name)
cy.get('tr').contains(user.last_name)
// Last active date
cy.get('tr').contains('21st Nov 2070')
// Managed status
cy.get('tr').contains('Managed')
// Dropdown button
cy.get('#managed-user-dropdown-some\\.user\\@example\\.com').should(
'exist'
)
})
})
describe('with a pending invite', function () {
let user: User
beforeEach(function () {
user = {
_id: 'some-user',
email: 'some.user@example.com',
first_name: 'Some',
last_name: 'User',
invite: true,
last_active_at: new Date('2070-11-21T03:00:00'),
enrollment: undefined,
isEntityAdmin: undefined,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<ManagedUserRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
it('should render a "Pending invite" badge', function () {
cy.get('.badge-new-comment').contains('Pending invite')
})
})
describe('with a group admin', function () {
let user: User
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: true,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<ManagedUserRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
it('should render a "Group admin" symbol', function () {
cy.get('[aria-label="Group admin"].fa-user-circle-o').should('exist')
})
})
describe('selecting and unselecting user row', function () {
let user: User
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,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<ManagedUserRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
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').should('be.checked')
cy.get('.select-item').click()
cy.get('.select-item').should('not.be.checked')
})
})
})

View File

@@ -1,7 +1,7 @@
import type { PropsWithChildren } from 'react'
import sinon from 'sinon'
import ManagedUserDropdownButton from '../../../../../../frontend/js/features/group-management/components/managed-users/managed-user-dropdown-button'
import { GroupMembersProvider } from '../../../../../../frontend/js/features/group-management/context/group-members-context'
import DropdownButton from '@/features/group-management/components/members-table/dropdown-button'
import { GroupMembersProvider } from '@/features/group-management/context/group-members-context'
import { User } from '../../../../../../types/group-management/user'
function Wrapper({ children }: PropsWithChildren<Record<string, unknown>>) {
@@ -20,7 +20,7 @@ function Wrapper({ children }: PropsWithChildren<Record<string, unknown>>) {
function mountDropDownComponent(user: User, subscriptionId: string) {
cy.mount(
<Wrapper>
<ManagedUserDropdownButton
<DropdownButton
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
@@ -218,6 +218,7 @@ describe('ManagedUserDropdownButton', function () {
cy.findByTestId('no-actions-available').should('exist')
})
})
describe('sending SSO invite reminder', function () {
const user = {
_id: 'some-user',

View File

@@ -1,7 +1,7 @@
import ManagedUserStatus from '../../../../../../frontend/js/features/group-management/components/managed-users/managed-user-status'
import ManagedUserStatus from '@/features/group-management/components/members-table/managed-user-status'
import { User } from '../../../../../../types/group-management/user'
describe('ManagedUserStatus', function () {
describe('MemberStatus', function () {
describe('with a pending invite', function () {
const user: User = {
_id: 'some-user',

View File

@@ -0,0 +1,685 @@
import sinon from 'sinon'
import MemberRow from '@/features/group-management/components/members-table/member-row'
import { GroupMembersProvider } from '@/features/group-management/context/group-members-context'
import { User } from '../../../../../../types/group-management/user'
describe('MemberRow', function () {
const subscriptionId = '123abc'
describe('default view', function () {
beforeEach(function () {
cy.window().then(win => {
win.metaAttributesCache = new Map()
})
})
describe('with an ordinary user', function () {
let user: User
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,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<MemberRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
it('renders the row', function () {
cy.get('tr').should('exist')
// Checkbox
cy.get('.select-item').should('not.be.checked')
// Email
cy.get('tr').contains(user.email)
// Name
cy.get('tr').contains(user.first_name)
cy.get('tr').contains(user.last_name)
// Last active date
cy.get('tr').contains('21st Nov 2070')
// Dropdown button
cy.get('#managed-user-dropdown-some\\.user\\@example\\.com').should(
'exist'
)
cy.get('tr').contains('SSO').should('not.exist')
cy.get('tr').contains('Managed').should('not.exist')
})
})
describe('with a pending invite', function () {
let user: User
beforeEach(function () {
user = {
_id: 'some-user',
email: 'some.user@example.com',
first_name: 'Some',
last_name: 'User',
invite: true,
last_active_at: new Date('2070-11-21T03:00:00'),
enrollment: undefined,
isEntityAdmin: undefined,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<MemberRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
it('should render a "Pending invite" badge', function () {
cy.get('.badge-new-comment').contains('Pending invite')
})
})
describe('with a group admin', function () {
let user: User
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: true,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<MemberRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
it('should render a "Group admin" symbol', function () {
cy.get('[aria-label="Group admin"].fa-user-circle-o').should('exist')
})
})
describe('selecting and unselecting user row', function () {
let user: User
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,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<MemberRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
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').should('be.checked')
cy.get('.select-item').click()
cy.get('.select-item').should('not.be.checked')
})
})
})
describe('with Managed Users enabled', function () {
beforeEach(function () {
cy.window().then(win => {
win.metaAttributesCache = new Map()
win.metaAttributesCache.set('ol-managedUsersActive', true)
})
})
describe('with an ordinary user', function () {
let user: User
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,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<MemberRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
it('renders the row', function () {
cy.get('tr').should('exist')
// Checkbox
cy.get('.select-item').should('not.be.checked')
// Email
cy.get('tr').contains(user.email)
// Name
cy.get('tr').contains(user.first_name)
cy.get('tr').contains(user.last_name)
// Last active date
cy.get('tr').contains('21st Nov 2070')
// Managed status
cy.get('tr').contains('Managed')
// Dropdown button
cy.get('#managed-user-dropdown-some\\.user\\@example\\.com').should(
'exist'
)
})
})
describe('with a pending invite', function () {
let user: User
beforeEach(function () {
user = {
_id: 'some-user',
email: 'some.user@example.com',
first_name: 'Some',
last_name: 'User',
invite: true,
last_active_at: new Date('2070-11-21T03:00:00'),
enrollment: undefined,
isEntityAdmin: undefined,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<MemberRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
it('should render a "Pending invite" badge', function () {
cy.get('.badge-new-comment').contains('Pending invite')
})
})
describe('with a group admin', function () {
let user: User
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: true,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<MemberRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
it('should render a "Group admin" symbol', function () {
cy.get('[aria-label="Group admin"].fa-user-circle-o').should('exist')
})
})
describe('selecting and unselecting user row', function () {
let user: User
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,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<MemberRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
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').should('be.checked')
cy.get('.select-item').click()
cy.get('.select-item').should('not.be.checked')
})
})
})
describe('with Group SSO enabled', function () {
beforeEach(function () {
cy.window().then(win => {
win.metaAttributesCache = new Map()
win.metaAttributesCache.set('ol-groupSSOActive', true)
})
})
describe('with an ordinary user', function () {
let user: User
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,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<MemberRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
it('renders the row', function () {
cy.get('tr').should('exist')
// Checkbox
cy.get('.select-item').should('not.be.checked')
// Email
cy.get('tr').contains(user.email)
// Name
cy.get('tr').contains(user.first_name)
cy.get('tr').contains(user.last_name)
// Last active date
cy.get('tr').contains('21st Nov 2070')
// SSO status
cy.get('tr').contains('SSO')
// Dropdown button
cy.get('#managed-user-dropdown-some\\.user\\@example\\.com').should(
'exist'
)
cy.get('tr').contains('Managed').should('not.exist')
})
})
describe('with a pending invite', function () {
let user: User
beforeEach(function () {
user = {
_id: 'some-user',
email: 'some.user@example.com',
first_name: 'Some',
last_name: 'User',
invite: true,
last_active_at: new Date('2070-11-21T03:00:00'),
enrollment: undefined,
isEntityAdmin: undefined,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<MemberRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
it('should render a "Pending invite" badge', function () {
cy.get('.badge-new-comment').contains('Pending invite')
})
})
describe('with a group admin', function () {
let user: User
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: true,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<MemberRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
it('should render a "Group admin" symbol', function () {
cy.get('[aria-label="Group admin"].fa-user-circle-o').should('exist')
})
})
describe('selecting and unselecting user row', function () {
let user: User
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,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<MemberRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
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').should('be.checked')
cy.get('.select-item').click()
cy.get('.select-item').should('not.be.checked')
})
})
})
describe('with Managed Users and Group SSO enabled', function () {
beforeEach(function () {
cy.window().then(win => {
win.metaAttributesCache = new Map()
win.metaAttributesCache.set('ol-managedUsersActive', true)
win.metaAttributesCache.set('ol-groupSSOActive', true)
})
})
describe('with an ordinary user', function () {
let user: User
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,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<MemberRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
it('renders the row', function () {
cy.get('tr').should('exist')
// Checkbox
cy.get('.select-item').should('not.be.checked')
// Email
cy.get('tr').contains(user.email)
// Name
cy.get('tr').contains(user.first_name)
cy.get('tr').contains(user.last_name)
// Last active date
cy.get('tr').contains('21st Nov 2070')
// Managed status
cy.get('tr').contains('Managed')
// SSO status
cy.get('tr').contains('SSO')
// Dropdown button
cy.get('#managed-user-dropdown-some\\.user\\@example\\.com').should(
'exist'
)
})
})
describe('with a pending invite', function () {
let user: User
beforeEach(function () {
user = {
_id: 'some-user',
email: 'some.user@example.com',
first_name: 'Some',
last_name: 'User',
invite: true,
last_active_at: new Date('2070-11-21T03:00:00'),
enrollment: undefined,
isEntityAdmin: undefined,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<MemberRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
it('should render a "Pending invite" badge', function () {
cy.get('.badge-new-comment').contains('Pending invite')
})
})
describe('with a group admin', function () {
let user: User
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: true,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<MemberRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
it('should render a "Group admin" symbol', function () {
cy.get('[aria-label="Group admin"].fa-user-circle-o').should('exist')
})
})
describe('selecting and unselecting user row', function () {
let user: User
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,
}
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<GroupMembersProvider>
<MemberRow
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</GroupMembersProvider>
)
})
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').should('be.checked')
cy.get('.select-item').click()
cy.get('.select-item').should('not.be.checked')
})
})
})
})

View File

@@ -1,17 +1,17 @@
import ManagedUsersList from '../../../../../../frontend/js/features/group-management/components/managed-users/managed-users-list'
import { GroupMembersProvider } from '../../../../../../frontend/js/features/group-management/context/group-members-context'
import MembersList from '@/features/group-management/components/members-table/members-list'
import { GroupMembersProvider } from '@/features/group-management/context/group-members-context'
const groupId = 'somegroup'
function mountManagedUsersList() {
cy.mount(
<GroupMembersProvider>
<ManagedUsersList groupId={groupId} />
<MembersList groupId={groupId} />
</GroupMembersProvider>
)
}
describe('ManagedUsersList', function () {
describe('MembersList', function () {
describe('with users', function () {
const users = [
{
@@ -98,7 +98,7 @@ describe('ManagedUsersList', function () {
})
cy.mount(
<GroupMembersProvider>
<ManagedUsersList groupId={groupId} />
<MembersList groupId={groupId} />
</GroupMembersProvider>
)
})

View File

@@ -1,4 +1,4 @@
import OffboardManagedUserModal from '../../../../../../frontend/js/features/group-management/components/managed-users/offboard-managed-user-modal'
import OffboardManagedUserModal from '@/features/group-management/components/members-table/offboard-managed-user-modal'
import sinon from 'sinon'
describe('OffboardManagedUserModal', function () {

View File

@@ -1,4 +1,4 @@
import PublisherManagers from '../../../../../frontend/js/features/group-management/components/publisher-managers'
import PublisherManagers from '@/features/group-management/components/publisher-managers'
const JOHN_DOE = {
_id: 'abc123def456',