Merge pull request #23743 from overleaf/ii-bs5-manage-group-members

[web] BS5 Group members management

GitOrigin-RevId: fab24ee6f6de07aa64887e123df930593fcec6a2
This commit is contained in:
ilkin-overleaf
2025-02-26 11:43:01 +02:00
committed by Copybot
parent 685207889c
commit a9ddf24343
27 changed files with 1314 additions and 823 deletions
@@ -38,6 +38,8 @@ async function manageGroupMembers(req, res, next) {
'flexible-group-licensing'
)
await SplitTestHandler.promises.getAssignment(req, res, 'bootstrap-5-groups')
const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
const userId = SessionManager.getLoggedInUserId(req.session)
const isAdmin = subscription.admin_id.toString() === userId
@@ -2,6 +2,10 @@ extends ../layout-marketing
block entrypointVar
- entrypoint = 'pages/user/subscription/group-management/group-members'
block vars
- bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly'
- bootstrap5PageSplitTest = 'bootstrap-5-groups'
block append meta
meta(name="ol-users", data-type="json", content=users)
@@ -0,0 +1,34 @@
import MaterialIcon from '@/shared/components/material-icon'
import IconButton from '@/features/ui/components/bootstrap-5/icon-button'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
type BackButtonProps = {
href: string
accessibilityLabel: string
}
function BackButton({ href, accessibilityLabel }: BackButtonProps) {
return (
<BootstrapVersionSwitcher
bs3={
<a href={href} className="back-btn">
<MaterialIcon
type="arrow_back"
accessibilityLabel={accessibilityLabel}
/>
</a>
}
bs5={
<IconButton
variant="ghost"
href={href}
size="lg"
icon="arrow_back"
accessibilityLabel={accessibilityLabel}
/>
}
/>
)
}
export default BackButton
@@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next'
import OLNotification from '@/features/ui/components/ol/ol-notification'
export type APIError = {
message?: string
@@ -17,15 +18,14 @@ export default function ErrorAlert({ error }: ErrorAlertProps) {
if (error.message) {
return (
<div className="alert alert-danger">
{t('error')}: {error.message}
</div>
<OLNotification
type="error"
content={`${t('error')}: ${error.message}`}
/>
)
}
return (
<div className="alert alert-danger">
{t('generic_something_went_wrong')}
</div>
<OLNotification type="error" content={t('generic_something_went_wrong')} />
)
}
@@ -1,7 +1,5 @@
import React, { useCallback, useState } from 'react'
import { Button, Col, Form, FormControl, Row } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import MaterialIcon from '../../../shared/components/material-icon'
import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n'
import getMeta from '../../../utils/meta'
import { useGroupMembersContext } from '../context/group-members-context'
@@ -9,6 +7,13 @@ import ErrorAlert from './error-alert'
import MembersList from './members-table/members-list'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { sendMB } from '../../../infrastructure/event-tracking'
import BackButton from '@/features/group-management/components/back-button'
import OLRow from '@/features/ui/components/ol/ol-row'
import OLCol from '@/features/ui/components/ol/ol-col'
import OLCard from '@/features/ui/components/ol/ol-card'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
import OLFormText from '@/features/ui/components/ol/ol-form-text'
export default function GroupMembers() {
const { isReady } = useWaitForI18n()
@@ -48,7 +53,7 @@ export default function GroupMembers() {
return null
}
const onAddMembersSubmit = (e: React.FormEvent<Form>) => {
const onAddMembersSubmit = (e: React.FormEvent) => {
e.preventDefault()
addMembers(emailString)
}
@@ -98,36 +103,37 @@ export default function GroupMembers() {
return (
<div className="container">
<Row>
<Col md={10} mdOffset={1}>
<h1>
<a href="/user/subscription" className="back-btn">
<MaterialIcon
type="arrow_back"
accessibilityLabel={t('back_to_subscription')}
/>
</a>{' '}
{groupName || t('group_subscription')}
</h1>
<div className="card">
<div className="page-header">
<OLRow>
<OLCol lg={{ span: 10, offset: 1 }}>
<div className="group-heading" data-testid="group-heading">
<BackButton
href="/user/subscription"
accessibilityLabel={t('back_to_subscription')}
/>
<h1 className="heading">{groupName || t('group_subscription')}</h1>
</div>
<OLCard>
<div
className="page-header mb-4"
data-testid="page-header-members-details"
>
<div className="pull-right">
{selectedUsers.length === 0 && groupSizeDetails()}
{removeMemberLoading ? (
<Button bsStyle="danger" disabled>
<OLButton variant="danger" disabled>
{t('removing')}&hellip;
</Button>
</OLButton>
) : (
<>
{selectedUsers.length > 0 && (
<Button bsStyle="danger" onClick={removeMembers}>
<OLButton variant="danger" onClick={removeMembers}>
{t('remove_from_group')}
</Button>
</OLButton>
)}
</>
)}
</div>
<h3>{t('members_management')}</h3>
<h2 className="h3 mt-0">{t('members_management')}</h2>
</div>
<div className="row-spaced-small">
<ErrorAlert error={removeMemberError} />
@@ -145,58 +151,67 @@ export default function GroupMembers() {
: t('add_more_members')}
</p>
<ErrorAlert error={inviteError} />
<Form horizontal onSubmit={onAddMembersSubmit} className="form">
<Row>
<Col xs={6}>
<FormControl
<form onSubmit={onAddMembersSubmit}>
<OLRow>
<OLCol xs={6}>
<OLFormControl
type="input"
placeholder="jane@example.com, joe@example.com"
aria-describedby="add-members-description"
value={emailString}
onChange={handleEmailsChange}
/>
</Col>
<Col xs={4}>
{inviteMemberLoading ? (
<Button bsStyle="primary" disabled>
{isFlexibleGroupLicensing
? t('inviting')
: t('adding')}
&hellip;
</Button>
) : (
<Button bsStyle="primary" onClick={onAddMembersSubmit}>
{isFlexibleGroupLicensing ? t('invite') : t('add')}
</Button>
)}
</Col>
<Col xs={2}>
</OLCol>
<OLCol xs={4}>
<OLButton
variant="primary"
onClick={onAddMembersSubmit}
isLoading={inviteMemberLoading}
bs3Props={{
loading: inviteMemberLoading ? (
<>
{isFlexibleGroupLicensing
? t('inviting')
: t('adding')}
&hellip;
</>
) : isFlexibleGroupLicensing ? (
t('invite')
) : (
t('add')
),
}}
>
{isFlexibleGroupLicensing ? t('invite') : t('add')}
</OLButton>
</OLCol>
<OLCol xs={2}>
<a href={paths.exportMembers}>{t('export_csv')}</a>
</Col>
</Row>
<Row>
<Col xs={8}>
<span className="help-block">
</OLCol>
</OLRow>
<OLRow>
<OLCol xs={8}>
<OLFormText bs3Props={{ className: 'help-block' }}>
{t('add_comma_separated_emails_help')}
</span>
</Col>
</Row>
</Form>
</OLFormText>
</OLCol>
</OLRow>
</form>
</div>
)}
{users.length >= groupSize && users.length > 0 && (
<>
<ErrorAlert error={inviteError} />
<Row>
<Col xs={2} xsOffset={10}>
<OLRow>
<OLCol xs={{ span: 2, offset: 10 }}>
<a href={paths.exportMembers}>{t('export_csv')}</a>
</Col>
</Row>
</OLCol>
</OLRow>
</>
)}
</div>
</Col>
</Row>
</OLCard>
</OLCol>
</OLRow>
</div>
)
}
@@ -6,7 +6,13 @@ import {
type SetStateAction,
} from 'react'
import { useTranslation } from 'react-i18next'
import { Dropdown, MenuItem } from 'react-bootstrap'
import { Dropdown as BS3Dropdown, MenuItem } from 'react-bootstrap'
import {
Dropdown,
DropdownItem,
DropdownMenu,
DropdownToggle,
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import { User } from '../../../../../../types/group-management/user'
import useAsync from '@/shared/hooks/use-async'
import { type FetchError, postJSON } from '@/infrastructure/fetch-json'
@@ -14,6 +20,10 @@ import Icon from '@/shared/components/icon'
import { GroupUserAlert } from '../../utils/types'
import { useGroupMembersContext } from '../../context/group-members-context'
import getMeta from '@/utils/meta'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import MaterialIcon from '@/shared/components/material-icon'
import DropdownListItem from '@/features/ui/components/bootstrap-5/dropdown-list-item'
import { Spinner } from 'react-bootstrap-5'
type resendInviteResponse = {
success: boolean
@@ -191,12 +201,10 @@ export default function DropdownButton({
<MenuItemButton
onClick={onResendGroupInviteClick}
key="resend-group-invite-action"
isLoading={isResendingGroupInvite}
data-testid="resend-group-invite-action"
>
{t('resend_group_invite')}
{isResendingGroupInvite ? (
<Icon type="spinner" spin style={{ marginLeft: '5px' }} />
) : null}
</MenuItemButton>
)
}
@@ -205,12 +213,10 @@ export default function DropdownButton({
<MenuItemButton
onClick={onResendManagedUserInviteClick}
key="resend-managed-user-invite-action"
isLoading={isResendingManagedUserInvite}
data-testid="resend-managed-user-invite-action"
>
{t('resend_managed_user_invite')}
{isResendingManagedUserInvite ? (
<Icon type="spinner" spin style={{ marginLeft: '5px' }} />
) : null}
</MenuItemButton>
)
}
@@ -230,12 +236,10 @@ export default function DropdownButton({
<MenuItemButton
onClick={onResendSSOLinkInviteClick}
key="resend-sso-link-invite-action"
isLoading={isResendingSSOLinkInvite}
data-testid="resend-sso-link-invite-action"
>
{t('resend_link_sso')}
{isResendingSSOLinkInvite ? (
<Icon type="spinner" spin style={{ marginLeft: '5px' }} />
) : null}
</MenuItemButton>
)
}
@@ -257,6 +261,7 @@ export default function DropdownButton({
data-testid="remove-user-action"
onClick={onRemoveFromGroup}
className="delete-user-action"
variant="danger"
>
{t('remove_from_group')}
</MenuItemButton>
@@ -265,54 +270,119 @@ export default function DropdownButton({
if (buttons.length === 0) {
buttons.push(
<MenuItem key="no-actions-available" data-testid="no-actions-available">
<span className="text-muted">{t('no_actions')}</span>
</MenuItem>
<BootstrapVersionSwitcher
bs3={
<MenuItem
key="no-actions-available"
data-testid="no-actions-available"
>
<span className="text-muted">{t('no_actions')}</span>
</MenuItem>
}
bs5={
<DropdownListItem>
<DropdownItem
as="button"
tabIndex={-1}
data-testid="no-actions-available"
disabled
>
{t('no_actions')}
</DropdownItem>
</DropdownListItem>
}
/>
)
}
return (
<span className="managed-user-actions">
<Dropdown
id={`managed-user-dropdown-${user.email}`}
open={isOpened}
onToggle={open => setIsOpened(open)}
>
<Dropdown.Toggle
bsStyle={null}
className="btn btn-link action-btn"
noCaret
>
<i
className="fa fa-ellipsis-v"
aria-hidden="true"
aria-label={t('actions')}
/>
</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right managed-user-dropdown-menu">
{buttons}
</Dropdown.Menu>
</Dropdown>
</span>
<BootstrapVersionSwitcher
bs3={
<div className="managed-user-actions">
<BS3Dropdown
id={`managed-user-dropdown-${user.email}`}
open={isOpened}
onToggle={open => setIsOpened(open)}
>
<BS3Dropdown.Toggle
bsStyle={null}
className="btn btn-link action-btn"
noCaret
>
<Icon type="ellipsis-v" accessibilityLabel={t('actions')} />
</BS3Dropdown.Toggle>
<BS3Dropdown.Menu className="dropdown-menu-right managed-user-dropdown-menu">
{buttons}
</BS3Dropdown.Menu>
</BS3Dropdown>
</div>
}
bs5={
<Dropdown align="end">
<DropdownToggle
id={`managed-user-dropdown-${user.email}`}
bsPrefix="dropdown-table-button-toggle"
>
<MaterialIcon type="more_vert" accessibilityLabel={t('actions')} />
</DropdownToggle>
<DropdownMenu flip={false}>{buttons}</DropdownMenu>
</Dropdown>
}
/>
)
}
type MenuItemButtonProps = {
isLoading?: boolean
'data-testid'?: string
} & Pick<ComponentProps<'button'>, 'children' | 'onClick' | 'className'> &
Pick<ComponentProps<typeof DropdownItem>, 'variant'>
function MenuItemButton({
children,
onClick,
className,
...buttonProps
}: ComponentProps<'button'>) {
isLoading,
variant,
'data-testid': dataTestId,
}: MenuItemButtonProps) {
return (
<li role="presentation" className={className}>
<button
className="managed-user-menu-item-button"
role="menuitem"
onClick={onClick}
{...buttonProps}
>
{children}
</button>
</li>
<BootstrapVersionSwitcher
bs3={
<li role="presentation" className={className}>
<button
className="managed-user-menu-item-button"
role="menuitem"
onClick={onClick}
data-testid={dataTestId}
>
{children}
</button>
</li>
}
bs5={
<DropdownListItem>
<DropdownItem
as="button"
tabIndex={-1}
onClick={onClick}
leadingIcon={
isLoading ? (
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
) : null
}
data-testid={dataTestId}
variant={variant}
>
{children}
</DropdownItem>
</DropdownListItem>
}
/>
)
}
@@ -1,8 +1,7 @@
import { type PropsWithChildren, useState } from 'react'
import { Alert, type AlertProps } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { Trans } from 'react-i18next'
import type { GroupUserAlertVariant } from '../../utils/types'
import NotificationScrolledTo from '@/shared/components/notification-scrolled-to'
import OLNotification from '@/features/ui/components/ol/ol-notification'
type GroupUsersListAlertProps = {
variant: GroupUserAlertVariant
@@ -86,20 +85,25 @@ function ResendManagedUserInviteSuccess({
userEmail,
}: GroupUsersListAlertComponentProps) {
return (
<AlertComponent bsStyle="success" onDismiss={onDismiss}>
<Trans
i18nKey="managed_user_invite_has_been_sent_to_email"
values={{
email: userEmail,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</AlertComponent>
<OLNotification
type="success"
content={
<Trans
i18nKey="managed_user_invite_has_been_sent_to_email"
values={{
email: userEmail,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
}
isDismissible
onDismiss={onDismiss}
/>
)
}
@@ -108,20 +112,25 @@ function ResendSSOLinkInviteSuccess({
userEmail,
}: GroupUsersListAlertComponentProps) {
return (
<AlertComponent bsStyle="success" onDismiss={onDismiss}>
<Trans
i18nKey="sso_link_invite_has_been_sent_to_email"
values={{
email: userEmail,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</AlertComponent>
<OLNotification
type="success"
content={
<Trans
i18nKey="sso_link_invite_has_been_sent_to_email"
values={{
email: userEmail,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
}
isDismissible
onDismiss={onDismiss}
/>
)
}
@@ -130,20 +139,25 @@ function FailedToResendManagedInvite({
userEmail,
}: GroupUsersListAlertComponentProps) {
return (
<AlertComponent bsStyle="danger" onDismiss={onDismiss}>
<Trans
i18nKey="failed_to_send_managed_user_invite_to_email"
values={{
email: userEmail,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</AlertComponent>
<OLNotification
type="error"
content={
<Trans
i18nKey="failed_to_send_managed_user_invite_to_email"
values={{
email: userEmail,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
}
isDismissible
onDismiss={onDismiss}
/>
)
}
function FailedToResendSSOLink({
@@ -151,20 +165,25 @@ function FailedToResendSSOLink({
userEmail,
}: GroupUsersListAlertComponentProps) {
return (
<AlertComponent bsStyle="danger" onDismiss={onDismiss}>
<Trans
i18nKey="failed_to_send_sso_link_invite_to_email"
values={{
email: userEmail,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</AlertComponent>
<OLNotification
type="error"
content={
<Trans
i18nKey="failed_to_send_sso_link_invite_to_email"
values={{
email: userEmail,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
}
isDismissible
onDismiss={onDismiss}
/>
)
}
@@ -173,20 +192,25 @@ function ResendGroupInviteSuccess({
userEmail,
}: GroupUsersListAlertComponentProps) {
return (
<AlertComponent bsStyle="success" onDismiss={onDismiss}>
<Trans
i18nKey="group_invite_has_been_sent_to_email"
values={{
email: userEmail,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</AlertComponent>
<OLNotification
type="success"
content={
<Trans
i18nKey="group_invite_has_been_sent_to_email"
values={{
email: userEmail,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
}
isDismissible
onDismiss={onDismiss}
/>
)
}
@@ -195,20 +219,25 @@ function FailedToResendGroupInvite({
userEmail,
}: GroupUsersListAlertComponentProps) {
return (
<AlertComponent bsStyle="danger" onDismiss={onDismiss}>
<Trans
i18nKey="failed_to_send_group_invite_to_email"
values={{
email: userEmail,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</AlertComponent>
<OLNotification
type="error"
content={
<Trans
i18nKey="failed_to_send_group_invite_to_email"
values={{
email: userEmail,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
}
isDismissible
onDismiss={onDismiss}
/>
)
}
@@ -217,53 +246,24 @@ function TooManyRequests({
userEmail,
}: GroupUsersListAlertComponentProps) {
return (
<AlertComponent bsStyle="danger" onDismiss={onDismiss}>
<Trans
i18nKey="an_email_has_already_been_sent_to"
values={{
email: userEmail,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</AlertComponent>
)
}
type AlertComponentProps = PropsWithChildren<{
bsStyle: AlertProps['bsStyle']
onDismiss: AlertProps['onDismiss']
}>
function AlertComponent({ bsStyle, onDismiss, children }: AlertComponentProps) {
const [show, setShow] = useState(true)
const { t } = useTranslation()
const handleDismiss = () => {
if (onDismiss) {
onDismiss()
}
setShow(false)
}
if (!show) {
return null
}
return (
<Alert bsStyle={bsStyle} className="managed-users-list-alert">
<span>{children}</span>
<div className="managed-users-list-alert-close">
<button type="button" className="close" onClick={handleDismiss}>
<span aria-hidden="true">&times;</span>
<span className="sr-only">{t('close')}</span>
</button>
</div>
</Alert>
<OLNotification
type="error"
content={
<Trans
i18nKey="an_email_has_already_been_sent_to"
values={{
email: userEmail,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
}
isDismissible
onDismiss={onDismiss}
/>
)
}
@@ -2,14 +2,18 @@ import moment from 'moment'
import { type Dispatch, type SetStateAction } from 'react'
import { useTranslation } from 'react-i18next'
import { User } from '../../../../../../types/group-management/user'
import Badge from '../../../../shared/components/badge'
import Tooltip from '../../../../shared/components/tooltip'
import type { GroupUserAlert } from '../../utils/types'
import ManagedUserStatus from './managed-user-status'
import SSOStatus from './sso-status'
import DropdownButton from './dropdown-button'
import SelectUserCheckbox from './select-user-checkbox'
import getMeta from '@/utils/meta'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import OLTag from '@/features/ui/components/ol/ol-tag'
import Icon from '@/shared/components/icon'
import MaterialIcon from '@/shared/components/material-icon'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import classnames from 'classnames'
type ManagedUserRowProps = {
user: User
@@ -31,61 +35,80 @@ export default function MemberRow({
const groupSSOActive = getMeta('ol-groupSSOActive')
return (
<tr
key={`user-${user.email}`}
className={`managed-user-row ${user.invite ? 'text-muted' : ''}`}
>
<tr className="managed-user-row">
<SelectUserCheckbox user={user} />
<td className="cell-email">
<td
className={classnames('cell-email', {
'text-muted': user.invite,
})}
>
<span>
{user.email}
{user.invite ? (
<span>
{user.invite && (
<>
&nbsp;
<Tooltip
id={`pending-invite-symbol-${user._id}`}
<OLTooltip
id={`pending-invite-symbol-${user.email}`}
description={t('pending_invite')}
>
<Badge
bsStyle={null}
className="badge-tag-bs3"
aria-label={t('pending_invite')}
data-testid="badge-pending-invite"
>
<OLTag data-testid="badge-pending-invite">
{t('pending_invite')}
</Badge>
</Tooltip>
</span>
) : (
''
</OLTag>
</OLTooltip>
</>
)}
{user.isEntityAdmin && (
<span>
<>
&nbsp;
<Tooltip
id={`group-admin-symbol-${user._id}`}
<OLTooltip
id={`group-admin-symbol-${user.email}`}
description={t('group_admin')}
>
<i
className="fa fa-user-circle-o"
aria-hidden="true"
aria-label={t('group_admin')}
/>
</Tooltip>
</span>
<span data-testid="group-admin-symbol">
<BootstrapVersionSwitcher
bs3={
<Icon
type="user-circle-o"
fw
accessibilityLabel={t('group_admin')}
/>
}
bs5={
<MaterialIcon
type="account_circle"
accessibilityLabel={t('group_admin')}
className="align-middle"
/>
}
/>
</span>
</OLTooltip>
</>
)}
</span>
</td>
<td className="cell-name">
<td
className={classnames('cell-name', {
'text-muted': user.invite,
})}
>
{user.first_name} {user.last_name}
</td>
<td className="cell-last-active">
<td
className={classnames('cell-last-active', {
'text-muted': user.invite,
})}
>
{user.last_active_at
? moment(user.last_active_at).format('Do MMM YYYY')
: 'N/A'}
</td>
{groupSSOActive && (
<td className="cell-security">
<td
className={classnames('cell-security', {
'text-muted': user.invite,
})}
>
<div className="managed-user-security">
<SSOStatus user={user} />
</div>
@@ -1,8 +1,6 @@
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 { useGroupMembersContext } from '../../context/group-members-context'
import type { GroupUserAlert } from '../../utils/types'
import MemberRow from './member-row'
@@ -12,6 +10,8 @@ import SelectAllCheckbox from './select-all-checkbox'
import classNames from 'classnames'
import getMeta from '@/utils/meta'
import UnlinkUserModal from './unlink-user-modal'
import OLTable from '@/features/ui/components/ol/ol-table'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
type ManagedUsersListProps = {
groupId: string
@@ -38,9 +38,9 @@ export default function MembersList({ groupId }: ManagedUsersListProps) {
onDismiss={() => setGroupUserAlert(undefined)}
/>
)}
<ul
<OLTable
className={classNames(
'list-unstyled',
'managed-users-table',
'structured-list',
'managed-users-list',
{
@@ -48,71 +48,60 @@ export default function MembersList({ groupId }: ManagedUsersListProps) {
'group-sso-active': groupSSOActive,
}
)}
container={false}
hover
data-testid="managed-users-table"
>
<li className="container-fluid">
<Row id="managed-users-list-headers">
<Col xs={12}>
<table className="managed-users-table">
<thead>
<tr>
<SelectAllCheckbox />
<td className="cell-email">
<span className="header">{t('email')}</span>
</td>
<td className="cell-name">
<span className="header">{t('name')}</span>
</td>
<td className="cell-last-active">
<Tooltip
id="last-active-tooltip"
description={t('last_active_description')}
overlayProps={{
placement: 'left',
}}
>
<span className="header">
{t('last_active')}
<sup>(?)</sup>
</span>
</Tooltip>
</td>
{groupSSOActive && (
<td className="cell-security">
<span className="header">{t('security')}</span>
</td>
)}
{managedUsersActive && (
<td className="cell-managed">
<span className="header">{t('managed')}</span>
</td>
)}
<td />
</tr>
</thead>
<tbody>
{users.length === 0 && (
<tr>
<td className="text-center" colSpan={5}>
<small>{t('no_members')}</small>
</td>
</tr>
)}
{users.map((user: any) => (
<MemberRow
key={user.email}
user={user}
openOffboardingModalForUser={setUserToOffboard}
openUnlinkUserModal={setUserToUnlink}
setGroupUserAlert={setGroupUserAlert}
groupId={groupId}
/>
))}
</tbody>
</table>
</Col>
</Row>
</li>
</ul>
<thead>
<tr>
<th className="cell-checkbox">
<SelectAllCheckbox />
</th>
<th className="cell-email">{t('email')}</th>
<th className="cell-name">{t('name')}</th>
<th className="cell-last-active">
<OLTooltip
id="last-active-tooltip"
description={t('last_active_description')}
overlayProps={{
placement: 'left',
}}
>
<span>
{t('last_active')}
<sup>(?)</sup>
</span>
</OLTooltip>
</th>
{groupSSOActive && (
<th className="cell-security">{t('security')}</th>
)}
{managedUsersActive && (
<th className="cell-managed">{t('managed')}</th>
)}
<th />
</tr>
</thead>
<tbody>
{users.length === 0 && (
<tr>
<td className="text-center" colSpan={5}>
<small>{t('no_members')}</small>
</td>
</tr>
)}
{users.map(user => (
<MemberRow
key={user.email}
user={user}
openOffboardingModalForUser={setUserToOffboard}
openUnlinkUserModal={setUserToUnlink}
setGroupUserAlert={setGroupUserAlert}
groupId={groupId}
/>
))}
</tbody>
</OLTable>
{userToOffboard && (
<OffboardManagedUserModal
user={userToOffboard}
@@ -1,14 +1,4 @@
import { User } from '../../../../../../types/group-management/user'
import {
Alert,
Button,
ControlLabel,
Form,
FormControl,
FormGroup,
Modal,
} from 'react-bootstrap'
import AccessibleModal from '@/shared/components/accessible-modal'
import Icon from '@/shared/components/icon'
import { useState } from 'react'
import useAsync from '@/shared/hooks/use-async'
@@ -16,6 +6,18 @@ import { useTranslation } from 'react-i18next'
import { useLocation } from '@/shared/hooks/use-location'
import { FetchError, postJSON } from '@/infrastructure/fetch-json'
import { debugConsole } from '@/utils/debugging'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLNotification from '@/features/ui/components/ol/ol-notification'
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
import OLFormSelect from '@/features/ui/components/ol/ol-form-select'
type OffboardManagedUserModalProps = {
user: User
@@ -69,12 +71,12 @@ export default function OffboardManagedUserModal({
}
return (
<AccessibleModal id={`delete-user-modal-${user._id}`} show onHide={onClose}>
<Form id="delete-user-form" onSubmit={handleDeleteUserSubmit}>
<Modal.Header>
<Modal.Title>{t('delete_user')}</Modal.Title>
</Modal.Header>
<Modal.Body>
<OLModal id={`delete-user-modal-${user._id}`} show onHide={onClose}>
<form id="delete-user-form" onSubmit={handleDeleteUserSubmit}>
<OLModalHeader>
<OLModalTitle>{t('delete_user')}</OLModalTitle>
</OLModalHeader>
<OLModalBody>
<p>
{t('about_to_delete_user_preamble', {
userName: userFullName,
@@ -95,21 +97,14 @@ export default function OffboardManagedUserModal({
</p>
<strong>{t('transfer_this_users_projects')}</strong>
<p>{t('transfer_this_users_projects_description')}</p>
<FormGroup>
<ControlLabel htmlFor="recipient-select-input">
{t('select_a_new_owner_for_projects')}
</ControlLabel>
<FormControl
id="recipient-select-input"
className="form-control"
componentClass="select"
<OLFormGroup controlId="recipient-select-input">
<OLFormLabel>{t('select_a_new_owner_for_projects')}</OLFormLabel>
<OLFormSelect
aria-label={t('select_user')}
required
placeholder={t('choose_from_group_members')}
value={selectedRecipientId || ''}
onChange={(e: React.ChangeEvent<HTMLFormElement & FormControl>) =>
setSelectedRecipientId(e.target.value)
}
onChange={e => setSelectedRecipientId(e.target.value)}
>
<option hidden disabled value="">
{t('choose_from_group_members')}
@@ -119,47 +114,50 @@ export default function OffboardManagedUserModal({
{member.email}
</option>
))}
</FormControl>
</FormGroup>
</OLFormSelect>
</OLFormGroup>
<p>
<span>{t('all_projects_will_be_transferred_immediately')}</span>
</p>
<FormGroup>
<ControlLabel htmlFor="supplied-email-input">
<OLFormGroup controlId="supplied-email-input">
<OLFormLabel>
{t('confirm_delete_user_type_email_address', {
userName: userFullName,
})}
</ControlLabel>
<FormControl
id="supplied-email-input"
</OLFormLabel>
<OLFormControl
type="email"
aria-label={t('email')}
onChange={(e: React.ChangeEvent<HTMLFormElement & FormControl>) =>
setSuppliedEmail(e.target.value)
}
onChange={e => setSuppliedEmail(e.target.value)}
/>
</FormGroup>
{error && <Alert bsStyle="danger">{error}</Alert>}
</Modal.Body>
<Modal.Footer>
<FormGroup>
<Button onClick={onClose}>{t('cancel')}</Button>
<Button
type="submit"
bsStyle="danger"
disabled={isLoading || isSuccess || !shouldEnableDeleteUserButton}
>
{isLoading ? (
</OLFormGroup>
{error && (
<OLNotification type="error" content={error} className="mb-0" />
)}
</OLModalBody>
<OLModalFooter>
<OLButton variant="secondary" onClick={onClose}>
{t('cancel')}
</OLButton>
<OLButton
type="submit"
variant="danger"
disabled={isLoading || isSuccess || !shouldEnableDeleteUserButton}
isLoading={isLoading}
bs3Props={{
loading: isLoading ? (
<>
<Icon type="refresh" fw spin /> {t('deleting')}
</>
) : (
t('delete_user')
)}
</Button>
</FormGroup>
</Modal.Footer>
</Form>
</AccessibleModal>
),
}}
>
{t('delete_user')}
</OLButton>
</OLModalFooter>
</form>
</OLModal>
)
}
@@ -1,6 +1,8 @@
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useGroupMembersContext } from '../../context/group-members-context'
import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
export default function SelectAllCheckbox() {
const { t } = useTranslation()
@@ -28,18 +30,27 @@ export default function SelectAllCheckbox() {
}
return (
<td className="cell-checkbox">
<label htmlFor="select-all" className="sr-only">
{t('select_all')}
</label>
<input
className="select-all"
id="select-all"
type="checkbox"
autoComplete="off"
onChange={handleSelectAllNonManagedClick}
checked={selectedUsers.length === nonManagedUsers.length}
/>
</td>
<BootstrapVersionSwitcher
bs3={
<input
className="select-all"
type="checkbox"
autoComplete="off"
onChange={handleSelectAllNonManagedClick}
checked={selectedUsers.length === nonManagedUsers.length}
aria-label={t('select_all')}
data-testid="select-all-checkbox"
/>
}
bs5={
<OLFormCheckbox
autoComplete="off"
onChange={handleSelectAllNonManagedClick}
checked={selectedUsers.length === nonManagedUsers.length}
aria-label={t('select_all')}
data-testid="select-all-checkbox"
/>
}
/>
)
}
@@ -2,6 +2,8 @@ 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 OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
type ManagedUsersSelectUserCheckboxProps = {
user: User
@@ -39,21 +41,30 @@ export default function SelectUserCheckbox({
return (
<td className="cell-checkbox">
{/* the next check will hide the `checkbox` but still show the `td` */}
{/* the next check will hide the `checkbox` but still show the `th` */}
{user.enrollment?.managedBy ? null : (
<>
<label htmlFor={`select-user-${user.email}`} className="sr-only">
{t('select_user')}
</label>
<input
className="select-item"
id={`select-user-${user.email}`}
type="checkbox"
autoComplete="off"
checked={selected}
onChange={e => handleSelectUser(e, user)}
/>
</>
<BootstrapVersionSwitcher
bs3={
<input
className="select-item"
type="checkbox"
autoComplete="off"
checked={selected}
onChange={e => handleSelectUser(e, user)}
aria-label={t('select_user')}
data-testid="select-single-checkbox"
/>
}
bs5={
<OLFormCheckbox
autoComplete="off"
checked={selected}
onChange={e => handleSelectUser(e, user)}
aria-label={t('select_user')}
data-testid="select-single-checkbox"
/>
}
/>
)}
</td>
)
@@ -1,5 +1,3 @@
import { Modal } from 'react-bootstrap'
import AccessibleModal from '@/shared/components/accessible-modal'
import { useTranslation, Trans } from 'react-i18next'
import { User } from '../../../../../../types/group-management/user'
import getMeta from '@/utils/meta'
@@ -10,6 +8,13 @@ import NotificationScrolledTo from '@/shared/components/notification-scrolled-to
import { debugConsole } from '@/utils/debugging'
import { GroupUserAlert } from '../../utils/types'
import { useGroupMembersContext } from '../../context/group-members-context'
import OLModal, {
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import OLButton from '@/features/ui/components/ol/ol-button'
export type UnlinkUserModalProps = {
onClose: () => void
@@ -77,11 +82,11 @@ export default function UnlinkUserModal({
)
return (
<AccessibleModal show onHide={onClose}>
<Modal.Header>
<Modal.Title>{t('unlink_user')}</Modal.Title>
</Modal.Header>
<Modal.Body>
<OLModal show onHide={onClose}>
<OLModalHeader>
<OLModalTitle>{t('unlink_user')}</OLModalTitle>
</OLModalHeader>
<OLModalBody>
{hasError && (
<div className="mb-3">
<NotificationScrolledTo
@@ -101,19 +106,19 @@ export default function UnlinkUserModal({
tOptions={{ interpolation: { escapeValue: true } }}
/>
</p>
</Modal.Body>
<Modal.Footer>
<button className="btn btn-secondary" disabled={unlinkInFlight}>
</OLModalBody>
<OLModalFooter>
<OLButton variant="secondary" disabled={unlinkInFlight}>
{t('cancel')}
</button>
<button
className="btn btn-danger"
</OLButton>
<OLButton
variant="danger"
onClick={e => handleUnlink(e)}
disabled={unlinkInFlight}
>
{t('unlink_user')}
</button>
</Modal.Footer>
</AccessibleModal>
</OLButton>
</OLModalFooter>
</OLModal>
)
}
@@ -0,0 +1,19 @@
import { Card } from 'react-bootstrap-5'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { FC } from 'react'
import classNames from 'classnames'
const OLCard: FC<{ className?: string }> = ({ children, className }) => {
return (
<BootstrapVersionSwitcher
bs3={<div className={classNames('card', className)}>{children}</div>}
bs5={
<Card className={className}>
<Card.Body>{children}</Card.Body>
</Card>
}
/>
)
}
export default OLCard
@@ -1,6 +1,7 @@
import Table from '@/features/ui/components/bootstrap-5/table'
import { Table as BS3Table } from 'react-bootstrap'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
type OLFormProps = React.ComponentProps<typeof Table> & {
bs3Props?: React.ComponentProps<typeof BS3Table>
@@ -11,10 +12,12 @@ function OLTable(props: OLFormProps) {
const bs3FormProps: React.ComponentProps<typeof BS3Table> = {
bsClass: rest.className,
id: rest.id,
condensed: rest.size === 'sm',
children: rest.children,
responsive:
typeof rest.responsive !== 'string' ? rest.responsive : undefined,
...getAriaAndDataProps(rest),
...bs3Props,
}
@@ -41,6 +41,10 @@ function NotificationScrolledTo({ ...props }: NotificationProps) {
notificationProps.className = `${notificationProps.className} notification-with-scroll-margin`
return <Notification {...notificationProps} />
return (
<div className="notification-list">
<Notification {...notificationProps} />
</div>
)
}
export default NotificationScrolledTo
@@ -361,3 +361,22 @@
}
}
}
.group-heading {
display: flex;
align-items: center;
gap: var(--spacing-04);
margin-bottom: var(--spacing-06);
.heading {
margin: 0;
}
.back-btn {
display: flex;
align-items: center;
width: 42px;
height: 42px;
text-decoration: none;
}
}
@@ -43,3 +43,4 @@
@import 'invite';
@import 'upgrade-prompt';
@import 'integrations-panel';
@import 'group-members';
@@ -0,0 +1,161 @@
/* Styles for group-subscription members view */
.structured-list.managed-users-list {
/* Override scrolling behaviour on structured-list */
overflow: initial;
overflow-y: initial;
overflow-x: initial;
}
.managed-users-list {
vertical-align: middle;
.security-state-invite-pending {
color: var(--content-disabled);
}
.security-state-managed {
color: var(--content-positive);
}
.security-state-not-managed {
color: var(--content-danger);
}
.managed-user-row {
overflow-wrap: break-word;
}
.managed-user-security {
display: flex;
justify-content: space-between;
}
}
.managed-users-table {
width: 100%;
table-layout: fixed;
@include media-breakpoint-up(sm) {
.cell-checkbox {
width: 5%;
}
.cell-email {
width: 30%;
}
&.group-sso-active .cell-email {
width: 29%;
}
.cell-name {
width: 20%;
}
.cell-last-active {
width: 20%;
}
.cell-security {
width: 12%;
}
.cell-managed {
width: 15%;
}
.cell-dropdown {
width: 26%;
min-width: 36px;
text-align: right;
}
&.managed-users-active {
.cell-email {
width: 30%;
}
&.group-sso-active {
.cell-checkbox {
width: 3%;
}
.cell-email {
width: 37%;
}
.cell-last-active {
width: 16%;
}
.cell-name {
width: 18%;
}
}
}
}
@include media-breakpoint-up(xl) {
.cell-checkbox {
width: 5%;
}
.cell-email {
width: 41%;
}
&.group-sso-active .cell-email {
width: 41%;
}
.cell-name {
width: 20%;
}
.cell-last-active {
width: 15%;
}
.cell-security {
width: 10%;
}
.cell-managed {
width: 13%;
}
.cell-dropdown {
width: 19%;
min-width: 36px;
text-align: right;
}
&.managed-users-active {
.cell-email {
width: 41%;
}
&.group-sso-active {
.cell-checkbox {
width: 3%;
}
.cell-email {
width: 36%;
}
.cell-name {
width: 18%;
}
}
}
}
}
.managed-user-security {
.material-symbols {
position: relative;
top: 4px;
}
}
@@ -37,6 +37,14 @@
}
}
}
.dropdown {
.dropdown-table-button-toggle {
@include action-button;
padding: var(--spacing-04);
}
}
}
.table-container-bordered {
@@ -570,12 +570,6 @@
right: var(--spacing-04);
padding: 0 !important;
}
.dropdown-table-button-toggle {
@include action-button;
padding: var(--spacing-04);
}
}
}
}
@@ -296,7 +296,8 @@
gap: var(--spacing-04);
margin-bottom: var(--spacing-06);
h2 {
h2,
.heading {
@include heading-lg;
margin: 0;
@@ -61,19 +61,6 @@
}
}
.managed-users-list-alert {
display: flex;
justify-content: space-between;
.managed-users-list-alert-close {
padding-left: @padding-sm;
text-align: right;
width: 10%;
@media (min-width: @screen-sm-min) {
width: auto;
}
}
}
.managed-users-table {
width: 100%;
table-layout: fixed;
@@ -106,36 +93,24 @@
@media (min-width: @screen-xs) {
.cell-checkbox {
width: 5%;
.managed-users-active.group-sso-active & {
width: 3%;
}
}
.cell-email {
width: 50%;
.managed-users-active & {
width: 35%;
}
.group-sso-active & {
}
&.group-sso-active {
.cell-email {
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 {
@@ -150,33 +125,47 @@
width: 6%;
min-width: 25px;
}
&.managed-users-active {
.cell-email {
width: 35%;
}
&.group-sso-active {
.cell-checkbox {
width: 3%;
}
.cell-email {
width: 29%;
}
.cell-last-active {
width: 16%;
}
.cell-name {
width: 18%;
}
}
}
}
@media (min-width: @screen-lg) {
.cell-checkbox {
width: 5%;
.managed-users-active.group-sso-active & {
width: 3%;
}
}
.cell-email {
width: 55%;
.managed-users-active & {
width: 42%;
}
.group-sso-active & {
width: 45%;
}
.managed-users-active.group-sso-active & {
width: 36%;
}
}
&.group-sso-active .cell-email {
width: 45%;
}
.cell-name {
width: 20%;
.managed-users-active.group-sso-active & {
width: 18%;
}
}
.cell-last-active {
@@ -195,6 +184,26 @@
width: 5%;
min-width: 25px;
}
&.managed-users-active {
.cell-email {
width: 42%;
}
&.group-sso-active {
.cell-checkbox {
width: 3%;
}
.cell-email {
width: 36%;
}
.cell-name {
width: 18%;
}
}
}
}
}
@@ -59,27 +59,31 @@ describe('GroupMembers', function () {
})
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.findByRole('heading', { name: /my awesome team/i, level: 1 })
cy.findByTestId('page-header-members-details').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.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
})
cy.findByTestId('managed-users-table')
.find('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.findByTestId('badge-pending-invite').should(
'have.text',
'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.findByTestId('badge-pending-invite').should('not.exist')
cy.get('tr:nth-child(2)').within(() => {
cy.contains('bobby.lapointe@test.com')
cy.contains('Bobby Lapointe')
cy.contains('2nd Jan 2023')
cy.findByTestId('badge-pending-invite').should('not.exist')
})
})
})
})
it('sends an invite', function () {
@@ -96,16 +100,18 @@ describe('GroupMembers', function () {
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.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
cy.findByTestId('managed-users-table')
.find('tbody')
.within(() => {
cy.get('tr:nth-child(3)').within(() => {
cy.contains('someone.else@test.com')
cy.contains('N/A')
cy.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
})
})
})
})
it('tries to send an invite and displays the error', function () {
@@ -124,25 +130,29 @@ describe('GroupMembers', function () {
})
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.findByTestId('managed-users-table')
.find('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('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.findByTestId('managed-users-table')
.find('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('tr:nth-child(2)').within(() => {
cy.get('.select-item').should('be.checked')
})
})
})
it('remove a member', function () {
@@ -150,23 +160,27 @@ describe('GroupMembers', function () {
statusCode: 200,
})
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.get('.select-item').check()
cy.findByTestId('managed-users-table')
.find('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')
cy.findByTestId('managed-users-table')
.find('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 () {
@@ -174,11 +188,13 @@ describe('GroupMembers', function () {
statusCode: 500,
})
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.get('.select-item').check()
cy.findByTestId('managed-users-table')
.find('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')
@@ -241,35 +257,37 @@ describe('GroupMembers', function () {
cy.get('h1').contains('My Awesome Team')
cy.get('small').contains('You have added 3 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('.sr-only').contains('Pending invite')
cy.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
cy.get(`.security-state-invite-pending`).should('exist')
})
cy.findByTestId('managed-users-table')
.find('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.findByTestId('badge-pending-invite').should(
'have.text',
'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.findByTestId('badge-pending-invite').should('not.exist')
cy.get('.sr-only').contains('Not managed')
})
cy.get('tr:nth-child(2)').within(() => {
cy.contains('bobby.lapointe@test.com')
cy.contains('Bobby Lapointe')
cy.contains('2nd Jan 2023')
cy.findByTestId('badge-pending-invite').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.findByTestId('badge-pending-invite').should('not.exist')
cy.get('.sr-only').contains('Managed')
cy.get('tr:nth-child(3)').within(() => {
cy.contains('claire.jennings@test.com')
cy.contains('Claire Jennings')
cy.contains('3rd Jan 2023')
cy.findByTestId('badge-pending-invite').should('not.exist')
cy.get('.sr-only').contains('Managed')
})
})
})
})
it('sends an invite', function () {
@@ -286,18 +304,20 @@ describe('GroupMembers', function () {
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.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
cy.get(`.security-state-invite-pending`).should('exist')
cy.findByTestId('managed-users-table')
.find('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.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
cy.get(`.security-state-invite-pending`).should('exist')
})
})
})
})
it('tries to send an invite and displays the error', function () {
@@ -316,25 +336,29 @@ describe('GroupMembers', function () {
})
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.findByTestId('managed-users-table')
.find('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('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.findByTestId('managed-users-table')
.find('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('tr:nth-child(2)').within(() => {
cy.get('.select-item').should('be.checked')
})
})
cy.get('button').contains('Remove from group').click()
})
@@ -344,22 +368,26 @@ describe('GroupMembers', function () {
statusCode: 200,
})
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.get('.select-item').check()
cy.findByTestId('managed-users-table')
.find('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')
cy.findByTestId('managed-users-table')
.find('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 () {
@@ -367,12 +395,14 @@ describe('GroupMembers', function () {
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')
cy.findByTestId('managed-users-table')
.find('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 () {
@@ -380,11 +410,13 @@ describe('GroupMembers', function () {
statusCode: 500,
})
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.get('.select-item').check()
cy.findByTestId('managed-users-table')
.find('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()
})
@@ -448,17 +480,19 @@ describe('GroupMembers', function () {
})
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')
})
cy.findByTestId('managed-users-table')
.find('tbody')
.within(() => {
cy.get('tr:nth-child(2)').within(() => {
cy.contains('bobby.lapointe@test.com')
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 active')
cy.get('tr:nth-child(3)').within(() => {
cy.contains('claire.jennings@test.com')
cy.get('.sr-only').contains('SSO active')
})
})
})
})
})
@@ -86,36 +86,38 @@ describe('group members, with managed users', function () {
cy.get('h1').contains('My Awesome Team')
cy.get('small').contains('You have added 3 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('.sr-only').contains('Pending invite')
cy.findByTestId('managed-users-table')
.find('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.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
cy.get(`.security-state-invite-pending`).should('exist')
})
cy.findByTestId('badge-pending-invite').should(
'have.text',
'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.findByTestId('badge-pending-invite').should('not.exist')
cy.get('.sr-only').contains('Not managed')
})
cy.get('tr:nth-child(2)').within(() => {
cy.contains('bobby.lapointe@test.com')
cy.contains('Bobby Lapointe')
cy.contains('2nd Jan 2023')
cy.findByTestId('badge-pending-invite').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.findByTestId('badge-pending-invite').should('not.exist')
cy.get('.sr-only').contains('Managed')
cy.get('tr:nth-child(3)').within(() => {
cy.contains('claire.jennings@test.com')
cy.contains('Claire Jennings')
cy.contains('3rd Jan 2023')
cy.findByTestId('badge-pending-invite').should('not.exist')
cy.get('.sr-only').contains('Managed')
})
})
})
})
it('sends an invite', function () {
@@ -132,18 +134,20 @@ describe('group members, with managed users', function () {
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.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
cy.get(`.security-state-invite-pending`).should('exist')
cy.findByTestId('managed-users-table')
.find('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.findByTestId('badge-pending-invite').should(
'have.text',
'Pending invite'
)
cy.get(`.security-state-invite-pending`).should('exist')
})
})
})
})
it('tries to send an invite and displays the error', function () {
@@ -162,25 +166,29 @@ describe('group members, with managed users', function () {
})
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.findByTestId('managed-users-table')
.find('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('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.findByTestId('managed-users-table')
.find('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('tr:nth-child(2)').within(() => {
cy.get('.select-item').should('be.checked')
})
})
cy.get('button').contains('Remove from group').click()
})
@@ -190,22 +198,26 @@ describe('group members, with managed users', function () {
statusCode: 200,
})
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.get('.select-item').check()
cy.findByTestId('managed-users-table')
.find('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')
cy.findByTestId('managed-users-table')
.find('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 () {
@@ -213,12 +225,14 @@ describe('group members, with managed users', function () {
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')
cy.findByTestId('managed-users-table')
.find('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 () {
@@ -226,11 +240,13 @@ describe('group members, with managed users', function () {
statusCode: 500,
})
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.get('.select-item').check()
cy.findByTestId('managed-users-table')
.find('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()
})
@@ -259,17 +275,19 @@ describe('Group members when group SSO is enabled', function () {
win.metaAttributesCache.set('ol-groupSSOActive', false)
})
mountGroupMembersProvider()
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').should('not.exist')
})
cy.findByTestId('managed-users-table')
.find('tbody')
.within(() => {
cy.get('tr:nth-child(2)').within(() => {
cy.contains('bobby.lapointe@test.com')
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 active').should('not.exist')
cy.get('tr:nth-child(3)').within(() => {
cy.contains('claire.jennings@test.com')
cy.get('.sr-only').contains('SSO active').should('not.exist')
})
})
})
})
it('should display SSO Column when Group SSO is enabled', function () {
@@ -277,16 +295,18 @@ describe('Group members when group SSO is enabled', function () {
win.metaAttributesCache.set('ol-groupSSOActive', true)
})
mountGroupMembersProvider()
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')
})
cy.findByTestId('managed-users-table')
.find('tbody')
.within(() => {
cy.get('tr:nth-child(2)').within(() => {
cy.contains('bobby.lapointe@test.com')
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 active')
cy.get('tr:nth-child(3)').within(() => {
cy.contains('claire.jennings@test.com')
cy.get('.sr-only').contains('SSO active')
})
})
})
})
})
@@ -40,9 +40,9 @@ describe('MemberRow', function () {
})
it('renders the row', function () {
cy.get('tr').should('exist')
cy.get('tr')
// Checkbox
cy.get('.select-item').should('not.be.checked')
cy.findByTestId('select-single-checkbox').should('not.be.checked')
// Email
cy.get('tr').contains(user.email)
// Name
@@ -131,7 +131,9 @@ describe('MemberRow', function () {
})
it('should render a "Group admin" symbol', function () {
cy.get('[aria-label="Group admin"].fa-user-circle-o').should('exist')
cy.findByTestId('group-admin-symbol').within(() => {
cy.findByText(/group admin/i)
})
})
})
@@ -167,11 +169,11 @@ describe('MemberRow', 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').should('be.checked')
cy.get('.select-item').click()
cy.get('.select-item').should('not.be.checked')
cy.findByTestId('select-single-checkbox').should('not.be.checked')
cy.findByTestId('select-single-checkbox').click()
cy.findByTestId('select-single-checkbox').should('be.checked')
cy.findByTestId('select-single-checkbox').click()
cy.findByTestId('select-single-checkbox').should('not.be.checked')
})
})
})
@@ -217,7 +219,7 @@ describe('MemberRow', function () {
it('renders the row', function () {
cy.get('tr').should('exist')
// Checkbox
cy.get('.select-item').should('not.be.checked')
cy.findByTestId('select-single-checkbox').should('not.be.checked')
// Email
cy.get('tr').contains(user.email)
// Name
@@ -305,7 +307,9 @@ describe('MemberRow', function () {
})
it('should render a "Group admin" symbol', function () {
cy.get('[aria-label="Group admin"].fa-user-circle-o').should('exist')
cy.findByTestId('group-admin-symbol').within(() => {
cy.findByText(/group admin/i)
})
})
})
@@ -341,11 +345,11 @@ describe('MemberRow', 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').should('be.checked')
cy.get('.select-item').click()
cy.get('.select-item').should('not.be.checked')
cy.findByTestId('select-single-checkbox').should('not.be.checked')
cy.findByTestId('select-single-checkbox').click()
cy.findByTestId('select-single-checkbox').should('be.checked')
cy.findByTestId('select-single-checkbox').click()
cy.findByTestId('select-single-checkbox').should('not.be.checked')
})
})
})
@@ -389,9 +393,8 @@ describe('MemberRow', function () {
})
it('renders the row', function () {
cy.get('tr').should('exist')
// Checkbox
cy.get('.select-item').should('not.be.checked')
cy.findByTestId('select-single-checkbox').should('not.be.checked')
// Email
cy.get('tr').contains(user.email)
// Name
@@ -481,7 +484,9 @@ describe('MemberRow', function () {
})
it('should render a "Group admin" symbol', function () {
cy.get('[aria-label="Group admin"].fa-user-circle-o').should('exist')
cy.findByTestId('group-admin-symbol').within(() => {
cy.findByText(/group admin/i)
})
})
})
@@ -517,11 +522,11 @@ describe('MemberRow', 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').should('be.checked')
cy.get('.select-item').click()
cy.get('.select-item').should('not.be.checked')
cy.findByTestId('select-single-checkbox').should('not.be.checked')
cy.findByTestId('select-single-checkbox').click()
cy.findByTestId('select-single-checkbox').should('be.checked')
cy.findByTestId('select-single-checkbox').click()
cy.findByTestId('select-single-checkbox').should('not.be.checked')
})
})
})
@@ -566,9 +571,8 @@ describe('MemberRow', function () {
})
it('renders the row', function () {
cy.get('tr').should('exist')
// Checkbox
cy.get('.select-item').should('not.be.checked')
cy.findByTestId('select-single-checkbox').should('not.be.checked')
// Email
cy.get('tr').contains(user.email)
// Name
@@ -658,7 +662,9 @@ describe('MemberRow', function () {
})
it('should render a "Group admin" symbol', function () {
cy.get('[aria-label="Group admin"].fa-user-circle-o').should('exist')
cy.findByTestId('group-admin-symbol').within(() => {
cy.findByText(/group admin/i)
})
})
})
@@ -694,11 +700,11 @@ describe('MemberRow', 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').should('be.checked')
cy.get('.select-item').click()
cy.get('.select-item').should('not.be.checked')
cy.findByTestId('select-single-checkbox').should('not.be.checked')
cy.findByTestId('select-single-checkbox').click()
cy.findByTestId('select-single-checkbox').should('be.checked')
cy.findByTestId('select-single-checkbox').click()
cy.findByTestId('select-single-checkbox').should('not.be.checked')
})
})
})
@@ -50,46 +50,74 @@ describe('MembersList', function () {
win.metaAttributesCache.set('ol-groupSSOActive', false)
})
mountManagedUsersList()
cy.get('#managed-users-list-headers').should('exist')
// Select-all checkbox
cy.get('#managed-users-list-headers .select-all').should('exist')
cy.get('#managed-users-list-headers').contains('Email')
cy.get('#managed-users-list-headers').contains('Name')
cy.get('#managed-users-list-headers').contains('Last Active')
cy.get('#managed-users-list-headers')
.contains('Security')
.should('not.exist')
cy.findByTestId('managed-users-table').within(() => {
cy.findByTestId('select-all-checkbox')
})
cy.findByTestId('managed-users-table').should('contain.text', 'Email')
cy.findByTestId('managed-users-table').should('contain.text', 'Name')
cy.findByTestId('managed-users-table').should(
'contain.text',
'Last Active'
)
cy.findByTestId('managed-users-table').should(
'not.contain.text',
'Security'
)
})
it('should render the table headers with SSO Column', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-groupSSOActive', true)
})
mountManagedUsersList()
cy.get('#managed-users-list-headers').should('exist')
// Select-all checkbox
cy.get('#managed-users-list-headers .select-all').should('exist')
cy.findByTestId('managed-users-table').within(() => {
cy.findByTestId('select-all-checkbox')
})
cy.get('#managed-users-list-headers').contains('Email')
cy.get('#managed-users-list-headers').contains('Name')
cy.get('#managed-users-list-headers').contains('Last Active')
cy.get('#managed-users-list-headers').contains('Security')
cy.findByTestId('managed-users-table').should('contain.text', 'Email')
cy.findByTestId('managed-users-table').should('contain.text', 'Name')
cy.findByTestId('managed-users-table').should(
'contain.text',
'Last Active'
)
cy.findByTestId('managed-users-table').should('contain.text', 'Security')
})
it('should render the list of users', function () {
cy.get('.managed-users-list')
.find('.managed-user-row')
.should('have.length', 2)
cy.findByTestId('managed-users-table')
.find('tbody')
.within(() => {
cy.findAllByRole('row').should('have.length', 2)
})
// First user
cy.get('.managed-users-list').contains(users[0].email)
cy.get('.managed-users-list').contains(users[0].first_name)
cy.get('.managed-users-list').contains(users[0].last_name)
cy.findByTestId('managed-users-table').should(
'contain.text',
users[0].email
)
cy.findByTestId('managed-users-table').should(
'contain.text',
users[0].first_name
)
cy.findByTestId('managed-users-table').should(
'contain.text',
users[0].last_name
)
// Second user
cy.get('.managed-users-list').contains(users[1].email)
cy.get('.managed-users-list').contains(users[1].first_name)
cy.get('.managed-users-list').contains(users[1].last_name)
cy.findByTestId('managed-users-table').should(
'contain.text',
users[1].email
)
cy.findByTestId('managed-users-table').should(
'contain.text',
users[1].first_name
)
cy.findByTestId('managed-users-table').should(
'contain.text',
users[1].last_name
)
})
})
@@ -106,10 +134,17 @@ describe('MembersList', function () {
})
it('should render the list, with a "no members" message', function () {
cy.get('.managed-users-list').contains('No members')
cy.get('.managed-users-list')
.find('.managed-user-row')
.should('have.length', 0)
cy.findByTestId('managed-users-table').should(
'contain.text',
'No members'
)
cy.findByTestId('managed-users-table')
.find('tbody')
.within(() => {
cy.findAllByRole('row')
.should('have.length', 1)
.and('contain.text', 'No members')
})
})
})
@@ -188,27 +223,32 @@ describe('MembersList', function () {
describe('unlinking user', function () {
beforeEach(function () {
mountManagedUsersList()
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(3)').within(() => {
cy.get('.sr-only').contains('SSO active')
cy.get('.action-btn').click()
cy.findByTestId('unlink-user-action').click()
cy.findByTestId('managed-users-table')
.find('tbody')
.within(() => {
cy.get('tr:nth-child(3)').within(() => {
cy.findByText('SSO active')
cy.findByRole('button', { name: /actions/i }).click()
cy.findByTestId('unlink-user-action').click()
})
})
})
})
it('should show successs notification and update the user row after unlinking', function () {
cy.get('.modal').within(() => {
cy.get('.btn-danger').click()
cy.findByRole('dialog').within(() => {
cy.findByRole('button', { name: /unlink user/i }).click()
})
cy.get('.notification').contains(
cy.findByRole('alert').should(
'contain.text',
`SSO reauthentication request has been sent to ${USER_LINKED.email}`
)
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(3)').within(() => {
cy.get('.sr-only').contains('SSO not active')
cy.findByTestId('managed-users-table')
.find('tbody')
.within(() => {
cy.get('tr:nth-child(3)').within(() => {
cy.findByText('SSO not active')
})
})
})
})
})
@@ -222,57 +262,67 @@ describe('MembersList', function () {
describe('when user is not managed', function () {
beforeEach(function () {
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(3)').within(() => {
cy.get('.sr-only').contains('SSO active')
cy.get('.sr-only').contains('Not managed')
cy.get('.action-btn').click()
cy.findByTestId('unlink-user-action').click()
cy.findByTestId('managed-users-table')
.find('tbody')
.within(() => {
cy.get('tr:nth-child(3)').within(() => {
cy.findByText('SSO active')
cy.findByText('Not managed')
cy.findByRole('button', { name: /actions/i }).click()
cy.findByTestId('unlink-user-action').click()
})
})
})
})
it('should show successs notification and update the user row after unlinking', function () {
cy.get('.modal').within(() => {
cy.get('.btn-danger').click()
cy.findByRole('dialog').within(() => {
cy.findByRole('button', { name: /unlink user/i }).click()
})
cy.get('.notification').contains(
cy.findByRole('alert').should(
'contain.text',
`SSO reauthentication request has been sent to ${USER_LINKED.email}`
)
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(3)').within(() => {
cy.get('.sr-only').contains('SSO not active')
cy.get('.sr-only').contains('Not managed')
cy.findByTestId('managed-users-table')
.find('tbody')
.within(() => {
cy.get('tr:nth-child(3)').within(() => {
cy.findByText('SSO not active')
cy.findByText('Not managed')
})
})
})
})
})
describe('when user is managed', function () {
beforeEach(function () {
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(4)').within(() => {
cy.get('.sr-only').contains('SSO active')
cy.get('.sr-only').contains('Managed')
cy.get('.action-btn').click()
cy.findByTestId('unlink-user-action').click()
cy.findByTestId('managed-users-table')
.find('tbody')
.within(() => {
cy.get('tr:nth-child(4)').within(() => {
cy.findByText('SSO active')
cy.findAllByText('Managed')
cy.findByRole('button', { name: /actions/i }).click()
cy.findByTestId('unlink-user-action').click()
})
})
})
})
it('should show successs notification and update the user row after unlinking', function () {
cy.get('.modal').within(() => {
cy.get('.btn-danger').click()
cy.findByRole('dialog').within(() => {
cy.findByRole('button', { name: /unlink user/i }).click()
})
cy.get('.notification').contains(
cy.findByRole('alert').should(
'contain.text',
`SSO reauthentication request has been sent to ${USER_LINKED_AND_MANAGED.email}`
)
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(4)').within(() => {
cy.get('.sr-only').contains('SSO not active')
cy.get('.sr-only').contains('Managed')
cy.findByTestId('managed-users-table')
.find('tbody')
.within(() => {
cy.get('tr:nth-child(4)').within(() => {
cy.findByText('SSO not active')
cy.findAllByText('Managed')
})
})
})
})
})
})