mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #13483 from overleaf/jk-managed-users-group-management-ui
[web] Managed Users: Update Group Member Management UI GitOrigin-RevId: 6896951927f0e3220db59dda208d7cfe9c6c309e
This commit is contained in:
parent
c3a2786d82
commit
a14e2aecfb
24 changed files with 1237 additions and 63 deletions
|
@ -17,9 +17,15 @@ const EmailHelper = require('../Helpers/EmailHelper')
|
|||
const { csvAttachment } = require('../../infrastructure/Response')
|
||||
const { UserIsManagerError } = require('./UserMembershipErrors')
|
||||
const CSVParser = require('json2csv').Parser
|
||||
const Settings = require('@overleaf/settings')
|
||||
|
||||
function isManagedUsersActiveOnGroup(entity) {
|
||||
return !!(Settings.managedUsers?.enabled && entity.groupPolicy)
|
||||
}
|
||||
|
||||
async function manageGroupMembers(req, res, next) {
|
||||
const { entity, entityConfig } = req
|
||||
const managedUsersActive = isManagedUsersActiveOnGroup(entity)
|
||||
return entity.fetchV1Data(function (error, entity) {
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
|
@ -42,6 +48,7 @@ async function manageGroupMembers(req, res, next) {
|
|||
groupId: entityPrimaryKey,
|
||||
users,
|
||||
groupSize: entity.membersLimit,
|
||||
managedUsersActive,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
|
|
@ -106,7 +106,21 @@ function getPopulatedListOfMembers(entity, attributes, callback) {
|
|||
}
|
||||
}
|
||||
|
||||
async.map(userObjects, UserMembershipViewModel.buildAsync, callback)
|
||||
async.map(userObjects, UserMembershipViewModel.buildAsync, (err, users) => {
|
||||
if (err) {
|
||||
return callback(err)
|
||||
}
|
||||
for (const user of users) {
|
||||
if (
|
||||
user?._id &&
|
||||
entity?.admin_id &&
|
||||
user._id.toString() === entity.admin_id.toString()
|
||||
) {
|
||||
user.isEntityAdmin = true
|
||||
}
|
||||
}
|
||||
callback(null, users)
|
||||
})
|
||||
}
|
||||
|
||||
function addUserToEntity(entity, attribute, user, callback) {
|
||||
|
|
|
@ -39,6 +39,7 @@ module.exports = UserMembershipViewModel = {
|
|||
last_name: 1,
|
||||
lastLoggedIn: 1,
|
||||
lastActive: 1,
|
||||
enrollment: 1,
|
||||
}
|
||||
return UserGetter.getUser(userId, projection, function (error, user) {
|
||||
if (error != null || user == null) {
|
||||
|
@ -61,6 +62,12 @@ function buildUserViewModel(user, isInvite) {
|
|||
last_active_at: user.lastActive || user.lastLoggedIn || null,
|
||||
last_logged_in_at: user.lastLoggedIn || null,
|
||||
invite: isInvite,
|
||||
enrollment: user.enrollment
|
||||
? {
|
||||
managedBy: user.enrollment.managedBy,
|
||||
enrolledAt: user.enrollment.enrolledAt,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ block append meta
|
|||
meta(name="ol-groupId", data-type="string", content=groupId)
|
||||
meta(name="ol-groupName", data-type="string", content=name)
|
||||
meta(name="ol-groupSize", data-type="json", content=groupSize)
|
||||
meta(name="ol-managedUsersActive", data-type="boolean", content=managedUsersActive)
|
||||
|
||||
block content
|
||||
main.content.content-alt#subscription-manage-group-root
|
||||
|
|
|
@ -826,6 +826,10 @@ module.exports = {
|
|||
|
||||
// ID of the IEEE brand in the rails app
|
||||
ieeeBrandId: intFromEnv('IEEE_BRAND_ID', 15),
|
||||
|
||||
managedUsers: {
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.mergeWith = function (overrides) {
|
||||
|
|
|
@ -198,6 +198,7 @@
|
|||
"delete_projects": "",
|
||||
"delete_tag": "",
|
||||
"delete_token": "",
|
||||
"delete_user": "",
|
||||
"delete_your_account": "",
|
||||
"deleted_at": "",
|
||||
"deleted_by_on": "",
|
||||
|
@ -395,6 +396,7 @@
|
|||
"go_to_code_location_in_pdf": "",
|
||||
"go_to_pdf_location_in_code": "",
|
||||
"go_to_settings": "",
|
||||
"group_admin": "",
|
||||
"group_plan_tooltip": "",
|
||||
"group_plan_with_name_tooltip": "",
|
||||
"group_subscription": "",
|
||||
|
@ -577,6 +579,7 @@
|
|||
"manage_publisher_managers": "",
|
||||
"manage_sessions": "",
|
||||
"manage_subscription": "",
|
||||
"managed": "",
|
||||
"managed_users": "",
|
||||
"managed_users_explanation": "",
|
||||
"managers_management": "",
|
||||
|
@ -624,6 +627,7 @@
|
|||
"new_to_latex_look_at": "",
|
||||
"newsletter": "",
|
||||
"next_payment_of_x_collectected_on_y": "",
|
||||
"no_actions": "",
|
||||
"no_comments": "",
|
||||
"no_existing_password": "",
|
||||
"no_folder": "",
|
||||
|
@ -646,6 +650,7 @@
|
|||
"normal": "",
|
||||
"normally_x_price_per_month": "",
|
||||
"normally_x_price_per_year": "",
|
||||
"not_managed": "",
|
||||
"notification_project_invite_accepted_message": "",
|
||||
"notification_project_invite_message": "",
|
||||
"number_of_users": "",
|
||||
|
@ -689,6 +694,7 @@
|
|||
"pdf_viewer": "",
|
||||
"pdf_viewer_error": "",
|
||||
"pending_additional_licenses": "",
|
||||
"pending_invite": "",
|
||||
"percent_discount_for_groups": "",
|
||||
"plan": "",
|
||||
"plan_tooltip": "",
|
||||
|
@ -848,6 +854,7 @@
|
|||
"search_search_for": "",
|
||||
"search_whole_word": "",
|
||||
"search_within_selection": "",
|
||||
"security": "",
|
||||
"select_a_file": "",
|
||||
"select_a_file_figure_modal": "",
|
||||
"select_a_payment_method": "",
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
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 GroupMemberRow from './group-member-row'
|
||||
|
||||
type GroupMembersListProps = {
|
||||
handleSelectAllClick: (e: any) => void
|
||||
selectedUsers: User[]
|
||||
users: User[]
|
||||
selectUser: (user: User) => void
|
||||
unselectUser: (user: User) => void
|
||||
}
|
||||
|
||||
export default function GroupMembersList({
|
||||
handleSelectAllClick,
|
||||
selectedUsers,
|
||||
users,
|
||||
selectUser,
|
||||
unselectUser,
|
||||
}: GroupMembersListProps) {
|
||||
const { t } = useTranslation()
|
||||
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>
|
||||
)
|
||||
}
|
|
@ -8,13 +8,13 @@ import {
|
|||
postJSON,
|
||||
} from '../../../infrastructure/fetch-json'
|
||||
import MaterialIcon from '../../../shared/components/material-icon'
|
||||
import Tooltip from '../../../shared/components/tooltip'
|
||||
import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n'
|
||||
import getMeta from '../../../utils/meta'
|
||||
import { parseEmails } from '../utils/emails'
|
||||
import ErrorAlert, { APIError } from './error-alert'
|
||||
import GroupMemberRow from './group-member-row'
|
||||
import useUserSelection from '../hooks/use-user-selection'
|
||||
import ManagedUsersList from './managed-users/managed-users-list'
|
||||
import GroupMembersList from './group-members-list'
|
||||
|
||||
export default function GroupMembers() {
|
||||
const { isReady } = useWaitForI18n()
|
||||
|
@ -39,6 +39,7 @@ 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 paths = useMemo(
|
||||
() => ({
|
||||
|
@ -90,6 +91,9 @@ export default function GroupMembers() {
|
|||
setRemoveMemberError(undefined)
|
||||
;(async () => {
|
||||
for (const user of selectedUsers) {
|
||||
if (user?.enrollment?.managedBy) {
|
||||
continue
|
||||
}
|
||||
let url
|
||||
if (paths.removeInvite && user.invite && user._id == null) {
|
||||
url = `${paths.removeInvite}/${encodeURIComponent(user.email)}`
|
||||
|
@ -142,6 +146,13 @@ export default function GroupMembers() {
|
|||
return null
|
||||
}
|
||||
|
||||
const shouldShowRemoveUsersButton = () => {
|
||||
return (
|
||||
selectedUsers.length > 0 &&
|
||||
!selectedUsers.find((u: User) => !!u?.enrollment?.managedBy)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<Row>
|
||||
|
@ -173,7 +184,7 @@ export default function GroupMembers() {
|
|||
</Button>
|
||||
) : (
|
||||
<>
|
||||
{selectedUsers.length > 0 && (
|
||||
{shouldShowRemoveUsersButton() && (
|
||||
<Button bsStyle="danger" onClick={removeMembers}>
|
||||
{t('remove_from_group')}
|
||||
</Button>
|
||||
|
@ -185,67 +196,27 @@ export default function GroupMembers() {
|
|||
</div>
|
||||
<div className="row-spaced-small">
|
||||
<ErrorAlert error={removeMemberError} />
|
||||
<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>
|
||||
{managedUsersActive ? (
|
||||
<ManagedUsersList
|
||||
handleSelectAllClick={handleSelectAllClick}
|
||||
selectedUsers={selectedUsers}
|
||||
users={users}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
/>
|
||||
) : (
|
||||
<GroupMembersList
|
||||
handleSelectAllClick={handleSelectAllClick}
|
||||
selectedUsers={selectedUsers}
|
||||
users={users}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<hr />
|
||||
{users.length < groupSize && (
|
||||
<div>
|
||||
<div className="add-more-members-form">
|
||||
<p className="small">{t('add_more_members')}</p>
|
||||
<ErrorAlert error={inviteError} />
|
||||
<Form horizontal onSubmit={addMembers} className="form">
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import { Dropdown, MenuItem } from 'react-bootstrap'
|
||||
import { User } from '../../../../../../types/group-management/user'
|
||||
import ControlledDropdown from '../../../../shared/components/controlled-dropdown'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type ManagedUserDropdownButtonProps = {
|
||||
user: User
|
||||
}
|
||||
|
||||
export default function ManagedUserDropdownButton({
|
||||
user,
|
||||
}: ManagedUserDropdownButtonProps) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<ControlledDropdown id={`managed-user-dropdown-${user._id}`}>
|
||||
<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">
|
||||
{user.enrollment ? (
|
||||
<MenuItem className="delete-user-action">{t('delete_user')}</MenuItem>
|
||||
) : (
|
||||
<>
|
||||
<MenuItem className="no-actions-available">
|
||||
<span className="text-muted">{t('no_actions')}</span>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</Dropdown.Menu>
|
||||
</ControlledDropdown>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
import moment from 'moment'
|
||||
import { useCallback } from 'react'
|
||||
import { Col, Row } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { User } from '../../../../../../types/group-management/user'
|
||||
import Badge from '../../../../shared/components/badge'
|
||||
import ManagedUserDropdownButton from './managed-user-dropdown-button'
|
||||
import Tooltip from '../../../../shared/components/tooltip'
|
||||
import ManagedUserStatus from './managed-user-status'
|
||||
|
||||
type ManagedUserRowProps = {
|
||||
user: User
|
||||
selectUser: (user: User) => void
|
||||
unselectUser: (user: User) => void
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
export default function ManagedUserRow({
|
||||
user,
|
||||
selectUser,
|
||||
unselectUser,
|
||||
selected,
|
||||
}: ManagedUserRowProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleSelectUser = useCallback(
|
||||
(event, user) => {
|
||||
if (event.target.checked) {
|
||||
selectUser(user)
|
||||
} else {
|
||||
unselectUser(user)
|
||||
}
|
||||
},
|
||||
[selectUser, unselectUser]
|
||||
)
|
||||
|
||||
return (
|
||||
<li
|
||||
key={`user-${user.email}`}
|
||||
className={`managed-user-row ${user.invite ? 'text-muted' : ''}`}
|
||||
>
|
||||
<Row>
|
||||
<Col xs={6}>
|
||||
<label htmlFor={`select-user-${user.email}`} className="sr-only">
|
||||
{t('select_user')}
|
||||
</label>
|
||||
<input
|
||||
className="select-item"
|
||||
id={`select-user-${user.email}`}
|
||||
type="checkbox"
|
||||
checked={selected}
|
||||
onChange={e => handleSelectUser(e, user)}
|
||||
/>
|
||||
<span>
|
||||
{user.email}
|
||||
{user.invite ? (
|
||||
<span>
|
||||
|
||||
<Tooltip
|
||||
id={`pending-invite-symbol-${user._id}`}
|
||||
description={t('pending_invite')}
|
||||
>
|
||||
<Badge aria-label={t('pending_invite')}>
|
||||
{t('pending_invite')}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{user.isEntityAdmin && (
|
||||
<span>
|
||||
|
||||
<Tooltip
|
||||
id={`group-admin-symbol-${user._id}`}
|
||||
description={t('group_admin')}
|
||||
>
|
||||
<i
|
||||
className="fa fa-user-circle-o"
|
||||
aria-hidden="true"
|
||||
aria-label={t('group_admin')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
{user.first_name} {user.last_name}
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
{user.last_active_at
|
||||
? moment(user.last_active_at).format('Do MMM YYYY')
|
||||
: 'N/A'}
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<span className="pull-right">
|
||||
<ManagedUserStatus user={user} />
|
||||
<span className="managed-user-actions">
|
||||
<ManagedUserDropdownButton user={user} />
|
||||
</span>
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
</li>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { User } from '../../../../../../types/group-management/user'
|
||||
|
||||
type ManagedUserStatusProps = {
|
||||
user: User
|
||||
}
|
||||
export default function ManagedUserStatus({ user }: ManagedUserStatusProps) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<span>
|
||||
{user.isEntityAdmin ? (
|
||||
<>
|
||||
<span className="security-state-group-admin" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{user.invite ? (
|
||||
<span className="security-state-invite-pending">
|
||||
<i
|
||||
className="fa fa-clock-o"
|
||||
aria-hidden="true"
|
||||
aria-label={t('pending_invite')}
|
||||
/>
|
||||
|
||||
{t('managed')}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
{user.enrollment?.managedBy ? (
|
||||
<span className="security-state-managed">
|
||||
<i
|
||||
className="fa fa-check"
|
||||
aria-hidden="true"
|
||||
aria-label={t('managed')}
|
||||
/>
|
||||
|
||||
{t('managed')}
|
||||
</span>
|
||||
) : (
|
||||
<span className="security-state-not-managed">
|
||||
<i
|
||||
className="fa fa-times"
|
||||
aria-hidden="true"
|
||||
aria-label={t('not_managed')}
|
||||
/>
|
||||
|
||||
{t('managed')}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
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 ManagedUserRow from './managed-user-row'
|
||||
|
||||
type ManagedUsersListProps = {
|
||||
handleSelectAllClick: (e: any) => void
|
||||
selectedUsers: User[]
|
||||
users: User[]
|
||||
selectUser: (user: User) => void
|
||||
unselectUser: (user: User) => void
|
||||
}
|
||||
|
||||
export default function ManagedUsersList({
|
||||
handleSelectAllClick,
|
||||
selectedUsers,
|
||||
users,
|
||||
selectUser,
|
||||
unselectUser,
|
||||
}: ManagedUsersListProps) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<ul className="list-unstyled structured-list managed-users-list">
|
||||
<li className="container-fluid">
|
||||
<Row id="managed-users-list-headers">
|
||||
<Col xs={6}>
|
||||
<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={2}>
|
||||
<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('security')}</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) => (
|
||||
<ManagedUserRow
|
||||
key={user.email}
|
||||
user={user}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
selected={selectedUsers.includes(user)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
|
@ -63,6 +63,7 @@
|
|||
@import 'components/beta-badges.less';
|
||||
@import 'components/divider.less';
|
||||
@import 'components/split-menu.less';
|
||||
@import 'components/group-members.less';
|
||||
|
||||
// Components w/ JavaScript
|
||||
@import 'components/modals.less';
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/* 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 {
|
||||
.security-state-invite-pending {
|
||||
color: @text-muted;
|
||||
}
|
||||
.security-state-managed {
|
||||
color: @green;
|
||||
}
|
||||
.security-state-not-managed {
|
||||
color: @red;
|
||||
}
|
||||
.managed-user-actions {
|
||||
button.dropdown-toggle {
|
||||
color: @text-color;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
li > a {
|
||||
&:hover {
|
||||
background-color: @gray-lightest;
|
||||
}
|
||||
}
|
||||
.delete-user-action {
|
||||
a {
|
||||
color: @red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -340,6 +340,7 @@
|
|||
"delete_projects": "Delete Projects",
|
||||
"delete_tag": "Delete Tag",
|
||||
"delete_token": "Delete token",
|
||||
"delete_user": "Delete User",
|
||||
"delete_your_account": "Delete your account",
|
||||
"deleted_at": "Deleted At",
|
||||
"deleted_by_on": "Deleted by __name__ on __date__",
|
||||
|
@ -656,6 +657,7 @@
|
|||
"go_to_code_location_in_pdf": "Go to code location in PDF",
|
||||
"go_to_pdf_location_in_code": "Go to PDF location in code (Tip: double click on the PDF for best results)",
|
||||
"go_to_settings": "Go to settings",
|
||||
"group_admin": "Group admin",
|
||||
"group_admins_get_access_to": "Group admins get access to",
|
||||
"group_admins_get_access_to_info": "Special features available only on group plans.",
|
||||
"group_full": "This group is already full",
|
||||
|
@ -950,6 +952,7 @@
|
|||
"manage_publisher_managers": "Manage publisher managers",
|
||||
"manage_sessions": "Manage Your Sessions",
|
||||
"manage_subscription": "Manage Subscription",
|
||||
"managed": "Managed",
|
||||
"managed_users": "Managed Users",
|
||||
"managed_users_explanation": "Managed Users ensures you stay in control of your organization’s projects and who owns them. <0>Read more about Managed Users.</0>",
|
||||
"managers_cannot_remove_admin": "Admins cannot be removed",
|
||||
|
@ -1026,6 +1029,7 @@
|
|||
"next_payment_of_x_collectected_on_y": "The next payment of <0>__paymentAmmount__</0> will be collected on <1>__collectionDate__</1>.",
|
||||
"nl": "Dutch",
|
||||
"no": "Norwegian",
|
||||
"no_actions": "No actions",
|
||||
"no_comments": "No comments",
|
||||
"no_complicated_latex_install": "No complicated LaTeX installation",
|
||||
"no_existing_password": "Please use the password reset form to set your password",
|
||||
|
@ -1058,6 +1062,7 @@
|
|||
"normally_x_price_per_year": "Normally __price__ per year",
|
||||
"not_a_student_question": "Not a student?",
|
||||
"not_found_error_from_the_supplied_url": "The link to open this content on Overleaf pointed to a file that could not be found. If this keeps happening for links on a particular site, please report this to them.",
|
||||
"not_managed": "Not managed",
|
||||
"not_now": "Not now",
|
||||
"not_registered": "Not registered",
|
||||
"note_experiments_under_development": "<0>Please note</0> that experiments in this program are still being tested and actively developed. This means that they might <0>change</0>, be <0>removed</0> or <0>become part of a paid plan</0>",
|
||||
|
@ -1139,6 +1144,7 @@
|
|||
"pdf_viewer_error": "There was a problem displaying the PDF for this project.",
|
||||
"pending": "Pending",
|
||||
"pending_additional_licenses": "Your subscription is changing to include <0>__pendingAdditionalLicenses__</0> additional license(s) for a total of <1>__pendingTotalLicenses__</1> licenses.",
|
||||
"pending_invite": "Pending invite",
|
||||
"per_month": "per month",
|
||||
"per_year": "per year",
|
||||
"percent_discount_for_groups": "__appName__ offers a __percent__% educational discount for groups of __size__ or more.",
|
||||
|
|
|
@ -24,7 +24,7 @@ const PATHS = {
|
|||
exportMembers: `/manage/groups/${GROUP_ID}/members/export`,
|
||||
}
|
||||
|
||||
describe('group members', function () {
|
||||
describe('group members, without managed users', function () {
|
||||
beforeEach(function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache = new Map()
|
||||
|
|
|
@ -0,0 +1,224 @@
|
|||
import GroupMembers from '../../../../../../frontend/js/features/group-management/components/group-members'
|
||||
|
||||
const GROUP_ID = '777fff777fff'
|
||||
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'),
|
||||
},
|
||||
}
|
||||
const PATHS = {
|
||||
addMember: `/manage/groups/${GROUP_ID}/invites`,
|
||||
removeMember: `/manage/groups/${GROUP_ID}/user`,
|
||||
removeInvite: `/manage/groups/${GROUP_ID}/invites`,
|
||||
exportMembers: `/manage/groups/${GROUP_ID}/members/export`,
|
||||
}
|
||||
|
||||
describe('group members, with managed users', function () {
|
||||
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)
|
||||
// Managed Users is active on this group
|
||||
win.metaAttributesCache.set('ol-managedUsersActive', true)
|
||||
})
|
||||
|
||||
cy.mount(<GroupMembers />)
|
||||
})
|
||||
|
||||
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.managed-users-list').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="Pending invite"]`)
|
||||
|
||||
cy.get('.badge-new-comment').contains('Pending invite')
|
||||
cy.get(`.security-state-invite-pending`).should('exist')
|
||||
})
|
||||
|
||||
cy.get('li:nth-child(3)').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('i[aria-label="Not managed"]').should('exist')
|
||||
})
|
||||
|
||||
cy.get('li:nth-child(4)').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('i[aria-label="Managed"]').should('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').within(() => {
|
||||
cy.get('li:nth-child(5)').within(() => {
|
||||
cy.contains('someone.else@test.com')
|
||||
cy.contains('N/A')
|
||||
cy.get(`[aria-label="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').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('.select-all').click()
|
||||
|
||||
cy.get('ul.managed-users-list').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.managed-users-list').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 2 of 10 available members')
|
||||
cy.get('ul.managed-users-list').within(() => {
|
||||
cy.get('li:nth-child(2)').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').within(() => {
|
||||
// Select 'Claire Jennings', a managed user
|
||||
cy.get('li:nth-child(4)').within(() => {
|
||||
cy.get('.select-item').check()
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('button').contains('Remove from group').should('not.exist')
|
||||
})
|
||||
|
||||
it('does not show the remove-member button if any of the selected users are managed', function () {
|
||||
cy.intercept('DELETE', `${PATHS.removeMember}/abc123def456`, {
|
||||
statusCode: 200,
|
||||
})
|
||||
|
||||
cy.get('ul.managed-users-list').within(() => {
|
||||
// Select 'Claire Jennings', a managed user
|
||||
cy.get('li:nth-child(4)').within(() => {
|
||||
cy.get('.select-item').check()
|
||||
})
|
||||
// Select another user
|
||||
cy.get('li:nth-child(3)').within(() => {
|
||||
cy.get('.select-item').check()
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('button').contains('Remove from group').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').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')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,63 @@
|
|||
import ManagedUserDropdownButton from '../../../../../../frontend/js/features/group-management/components/managed-users/managed-user-dropdown-button'
|
||||
|
||||
describe('ManagedUserDropdownButton', function () {
|
||||
describe('with managed user', function () {
|
||||
const user = {
|
||||
_id: 'some-user',
|
||||
email: 'some.user@example.com',
|
||||
first_name: 'Some',
|
||||
last_name: 'User',
|
||||
invite: true,
|
||||
last_active_at: new Date(),
|
||||
enrollment: {
|
||||
managedBy: 'some-group',
|
||||
enrolledAt: new Date(),
|
||||
},
|
||||
isEntityAdmin: undefined,
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
cy.mount(<ManagedUserDropdownButton user={user} />)
|
||||
})
|
||||
|
||||
it('should render the button', function () {
|
||||
cy.get(`#managed-user-dropdown-${user._id}`).should('exist')
|
||||
cy.get(`.action-btn`).should('exist')
|
||||
})
|
||||
|
||||
it('should show the menu when the button is clicked', function () {
|
||||
cy.get('.action-btn').click()
|
||||
cy.get('.delete-user-action').should('exist')
|
||||
cy.get('.delete-user-action').then($el => {
|
||||
Cypress.dom.isVisible($el)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with non-managed user', function () {
|
||||
const user = {
|
||||
_id: 'some-user',
|
||||
email: 'some.user@example.com',
|
||||
first_name: 'Some',
|
||||
last_name: 'User',
|
||||
invite: true,
|
||||
last_active_at: new Date(),
|
||||
enrollment: undefined,
|
||||
isEntityAdmin: undefined,
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
cy.mount(<ManagedUserDropdownButton user={user} />)
|
||||
})
|
||||
|
||||
it('should render the button', function () {
|
||||
cy.get(`#managed-user-dropdown-${user._id}`).should('exist')
|
||||
cy.get(`.action-btn`).should('exist')
|
||||
})
|
||||
|
||||
it('should show the (empty) menu when the button is clicked', function () {
|
||||
cy.get('.action-btn').click()
|
||||
cy.get('.no-actions-available').should('exist')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,246 @@
|
|||
import sinon, { SinonStub } from 'sinon'
|
||||
import ManagedUserRow from '../../../../../../frontend/js/features/group-management/components/managed-users/managed-user-row'
|
||||
import { User } from '../../../../../../types/group-management/user'
|
||||
|
||||
describe('ManagedUserRow', function () {
|
||||
describe('with an ordinary user', function () {
|
||||
let user: User,
|
||||
selectUser: SinonStub,
|
||||
unselectUser: SinonStub,
|
||||
selected: boolean
|
||||
|
||||
beforeEach(function () {
|
||||
user = {
|
||||
_id: 'some-user',
|
||||
email: 'some.user@example.com',
|
||||
first_name: 'Some',
|
||||
last_name: 'User',
|
||||
invite: false,
|
||||
last_active_at: new Date('2070-11-21T03:00:00'),
|
||||
enrollment: undefined,
|
||||
isEntityAdmin: undefined,
|
||||
}
|
||||
selectUser = sinon.stub()
|
||||
unselectUser = sinon.stub()
|
||||
selected = false
|
||||
|
||||
cy.mount(
|
||||
<ManagedUserRow
|
||||
user={user}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
selected={selected}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
it('renders the row', function () {
|
||||
cy.get('.row').should('exist')
|
||||
// Checkbox
|
||||
cy.get('.select-item').should('not.be.checked')
|
||||
// Email
|
||||
cy.get('.row').contains(user.email)
|
||||
// Name
|
||||
cy.get('.row').contains(user.first_name)
|
||||
cy.get('.row').contains(user.last_name)
|
||||
// Last active date
|
||||
cy.get('.row').contains('21st Nov 2070')
|
||||
// Managed status
|
||||
cy.get('.row').contains('Managed')
|
||||
// Dropdown button
|
||||
cy.get(`#managed-user-dropdown-${user._id}`).should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a pending invite', function () {
|
||||
let user: User,
|
||||
selectUser: SinonStub,
|
||||
unselectUser: SinonStub,
|
||||
selected: boolean
|
||||
|
||||
beforeEach(function () {
|
||||
user = {
|
||||
_id: 'some-user',
|
||||
email: 'some.user@example.com',
|
||||
first_name: 'Some',
|
||||
last_name: 'User',
|
||||
invite: true,
|
||||
last_active_at: new Date('2070-11-21T03:00:00'),
|
||||
enrollment: undefined,
|
||||
isEntityAdmin: undefined,
|
||||
}
|
||||
selectUser = sinon.stub()
|
||||
unselectUser = sinon.stub()
|
||||
selected = false
|
||||
|
||||
cy.mount(
|
||||
<ManagedUserRow
|
||||
user={user}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
selected={selected}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
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,
|
||||
selectUser: SinonStub,
|
||||
unselectUser: SinonStub,
|
||||
selected: boolean
|
||||
|
||||
beforeEach(function () {
|
||||
user = {
|
||||
_id: 'some-user',
|
||||
email: 'some.user@example.com',
|
||||
first_name: 'Some',
|
||||
last_name: 'User',
|
||||
invite: false,
|
||||
last_active_at: new Date('2070-11-21T03:00:00'),
|
||||
enrollment: undefined,
|
||||
isEntityAdmin: true,
|
||||
}
|
||||
selectUser = sinon.stub()
|
||||
unselectUser = sinon.stub()
|
||||
selected = false
|
||||
|
||||
cy.mount(
|
||||
<ManagedUserRow
|
||||
user={user}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
selected={selected}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
it('should render a "Group admin" symbol', function () {
|
||||
cy.get('[aria-label="Group admin"].fa-user-circle-o').should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('user is selected', function () {
|
||||
let user: User,
|
||||
selectUser: SinonStub,
|
||||
unselectUser: SinonStub,
|
||||
selected: boolean
|
||||
|
||||
beforeEach(function () {
|
||||
user = {
|
||||
_id: 'some-user',
|
||||
email: 'some.user@example.com',
|
||||
first_name: 'Some',
|
||||
last_name: 'User',
|
||||
invite: false,
|
||||
last_active_at: new Date('2070-11-21T03:00:00'),
|
||||
enrollment: undefined,
|
||||
isEntityAdmin: undefined,
|
||||
}
|
||||
selectUser = sinon.stub()
|
||||
unselectUser = sinon.stub()
|
||||
// User is selected
|
||||
selected = true
|
||||
|
||||
cy.mount(
|
||||
<ManagedUserRow
|
||||
user={user}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
selected={selected}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
it('should render the selection box as selected', function () {
|
||||
cy.get('.select-item').should('be.checked')
|
||||
})
|
||||
})
|
||||
|
||||
describe('selecting user row', function () {
|
||||
let user: User,
|
||||
selectUser: SinonStub,
|
||||
unselectUser: SinonStub,
|
||||
selected: boolean
|
||||
|
||||
beforeEach(function () {
|
||||
user = {
|
||||
_id: 'some-user',
|
||||
email: 'some.user@example.com',
|
||||
first_name: 'Some',
|
||||
last_name: 'User',
|
||||
invite: false,
|
||||
last_active_at: new Date('2070-11-21T03:00:00'),
|
||||
enrollment: undefined,
|
||||
isEntityAdmin: undefined,
|
||||
}
|
||||
selectUser = sinon.stub()
|
||||
unselectUser = sinon.stub()
|
||||
// User is not selected
|
||||
selected = false
|
||||
|
||||
cy.mount(
|
||||
<ManagedUserRow
|
||||
user={user}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
selected={selected}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
it('should select the user', function () {
|
||||
cy.get('.select-item').should('not.be.checked')
|
||||
cy.get('.select-item').click()
|
||||
cy.get('.select-item').then(() => {
|
||||
expect(selectUser.called).to.equal(true)
|
||||
expect(unselectUser.called).to.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('un-selecting user row', function () {
|
||||
let user: User,
|
||||
selectUser: SinonStub,
|
||||
unselectUser: SinonStub,
|
||||
selected: boolean
|
||||
|
||||
beforeEach(function () {
|
||||
user = {
|
||||
_id: 'some-user',
|
||||
email: 'some.user@example.com',
|
||||
first_name: 'Some',
|
||||
last_name: 'User',
|
||||
invite: false,
|
||||
last_active_at: new Date('2070-11-21T03:00:00'),
|
||||
enrollment: undefined,
|
||||
isEntityAdmin: undefined,
|
||||
}
|
||||
selectUser = sinon.stub()
|
||||
unselectUser = sinon.stub()
|
||||
// User is selected
|
||||
selected = true
|
||||
|
||||
cy.mount(
|
||||
<ManagedUserRow
|
||||
user={user}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
selected={selected}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
it('should select the user', function () {
|
||||
cy.get('.select-item').should('be.checked')
|
||||
cy.get('.select-item').click()
|
||||
cy.get('.select-item').then(() => {
|
||||
expect(unselectUser.called).to.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,86 @@
|
|||
import ManagedUserStatus from '../../../../../../frontend/js/features/group-management/components/managed-users/managed-user-status'
|
||||
import { User } from '../../../../../../types/group-management/user'
|
||||
|
||||
describe('ManagedUserStatus', function () {
|
||||
describe('with a pending invite', function () {
|
||||
const user: User = {
|
||||
_id: 'some-user',
|
||||
email: 'some.user@example.com',
|
||||
first_name: 'Some',
|
||||
last_name: 'User',
|
||||
invite: true,
|
||||
last_active_at: new Date(),
|
||||
enrollment: undefined,
|
||||
isEntityAdmin: undefined,
|
||||
}
|
||||
beforeEach(function () {
|
||||
cy.mount(<ManagedUserStatus user={user} />)
|
||||
})
|
||||
|
||||
it('should render a pending state', function () {
|
||||
cy.get('.security-state-invite-pending').contains('Managed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with a managed user', function () {
|
||||
const user: User = {
|
||||
_id: 'some-user',
|
||||
email: 'some.user@example.com',
|
||||
first_name: 'Some',
|
||||
last_name: 'User',
|
||||
invite: false,
|
||||
last_active_at: new Date(),
|
||||
enrollment: { managedBy: 'some-group', enrolledAt: new Date() },
|
||||
isEntityAdmin: undefined,
|
||||
}
|
||||
beforeEach(function () {
|
||||
cy.mount(<ManagedUserStatus user={user} />)
|
||||
})
|
||||
|
||||
it('should render a pending state', function () {
|
||||
cy.get('.security-state-managed').contains('Managed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an un-managed user', function () {
|
||||
const user: User = {
|
||||
_id: 'some-user',
|
||||
email: 'some.user@example.com',
|
||||
first_name: 'Some',
|
||||
last_name: 'User',
|
||||
invite: false,
|
||||
last_active_at: new Date(),
|
||||
enrollment: undefined,
|
||||
isEntityAdmin: undefined,
|
||||
}
|
||||
beforeEach(function () {
|
||||
cy.mount(<ManagedUserStatus user={user} />)
|
||||
})
|
||||
|
||||
it('should render an un-managed state', function () {
|
||||
cy.get('.security-state-not-managed').contains('Managed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with the group admin', function () {
|
||||
const user: User = {
|
||||
_id: 'some-user',
|
||||
email: 'some.user@example.com',
|
||||
first_name: 'Some',
|
||||
last_name: 'User',
|
||||
invite: false,
|
||||
last_active_at: new Date(),
|
||||
enrollment: undefined,
|
||||
isEntityAdmin: true,
|
||||
}
|
||||
beforeEach(function () {
|
||||
cy.mount(<ManagedUserStatus user={user} />)
|
||||
})
|
||||
|
||||
it('should render no state indicator', function () {
|
||||
cy.get('.security-state-group-admin')
|
||||
.contains('Managed')
|
||||
.should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,97 @@
|
|||
import ManagedUsersList from '../../../../../../frontend/js/features/group-management/components/managed-users/managed-users-list'
|
||||
import { User } from '../../../../../../types/group-management/user'
|
||||
|
||||
describe('ManagedUsersList', function () {
|
||||
describe('with users', function () {
|
||||
const users: User[] = [
|
||||
{
|
||||
_id: 'user-one',
|
||||
email: 'sarah.brennan@example.com',
|
||||
first_name: 'Sarah',
|
||||
last_name: 'Brennan',
|
||||
invite: false,
|
||||
last_active_at: new Date('2070-10-22T03:00:00'),
|
||||
enrollment: undefined,
|
||||
isEntityAdmin: undefined,
|
||||
},
|
||||
{
|
||||
_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,
|
||||
},
|
||||
]
|
||||
const selectedUsers: User[] = []
|
||||
const handleSelectAllClick = () => {}
|
||||
const selectUser = () => {}
|
||||
const unselectUser = () => {}
|
||||
|
||||
beforeEach(function () {
|
||||
cy.mount(
|
||||
<ManagedUsersList
|
||||
users={users}
|
||||
selectedUsers={selectedUsers}
|
||||
handleSelectAllClick={handleSelectAllClick}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
it('should render the table headers', function () {
|
||||
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')
|
||||
})
|
||||
|
||||
it('should render the list of users', function () {
|
||||
cy.get('.managed-users-list')
|
||||
.find('.managed-user-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)
|
||||
// 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)
|
||||
})
|
||||
})
|
||||
describe('empty user list', function () {
|
||||
const users: User[] = []
|
||||
const selectedUsers: User[] = []
|
||||
const handleSelectAllClick = () => {}
|
||||
const selectUser = () => {}
|
||||
const unselectUser = () => {}
|
||||
|
||||
beforeEach(function () {
|
||||
cy.mount(
|
||||
<ManagedUsersList
|
||||
users={users}
|
||||
selectedUsers={selectedUsers}
|
||||
handleSelectAllClick={handleSelectAllClick}
|
||||
selectUser={selectUser}
|
||||
unselectUser={unselectUser}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -59,6 +59,12 @@ describe('UserMembershipController', function () {
|
|||
},
|
||||
]
|
||||
|
||||
this.Settings = {
|
||||
managedUsers: {
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
this.SessionManager = {
|
||||
getSessionUser: sinon.stub().returns(this.user),
|
||||
getLoggedInUserId: sinon.stub().returns(this.user._id),
|
||||
|
@ -84,6 +90,7 @@ describe('UserMembershipController', function () {
|
|||
'../Authentication/SessionManager': this.SessionManager,
|
||||
'../SplitTests/SplitTestHandler': this.SplitTestHandler,
|
||||
'./UserMembershipHandler': this.UserMembershipHandler,
|
||||
'@overleaf/settings': this.Settings,
|
||||
},
|
||||
}
|
||||
))
|
||||
|
@ -113,6 +120,20 @@ describe('UserMembershipController', function () {
|
|||
expect(viewPath).to.equal('user_membership/group-members-react')
|
||||
expect(viewParams.users).to.deep.equal(this.users)
|
||||
expect(viewParams.groupSize).to.equal(this.subscription.membersLimit)
|
||||
expect(viewParams.managedUsersActive).to.equal(false)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('render group view with managed users', async function () {
|
||||
this.req.entity.groupPolicy = { somePolicy: true }
|
||||
this.Settings.managedUsers.enabled = true
|
||||
return await this.UserMembershipController.manageGroupMembers(this.req, {
|
||||
render: (viewPath, viewParams) => {
|
||||
expect(viewPath).to.equal('user_membership/group-members-react')
|
||||
expect(viewParams.users).to.deep.equal(this.users)
|
||||
expect(viewParams.groupSize).to.equal(this.subscription.membersLimit)
|
||||
expect(viewParams.managedUsersActive).to.equal(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
@ -39,6 +39,10 @@ describe('UserMembershipViewModel', function () {
|
|||
email: 'mock-email@baz.com',
|
||||
first_name: 'Name',
|
||||
lastLoggedIn: '2020-05-20T10:41:11.407Z',
|
||||
enrollment: {
|
||||
managedBy: 'mock-group-id',
|
||||
enrolledAt: new Date(),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -53,6 +57,7 @@ describe('UserMembershipViewModel', function () {
|
|||
first_name: null,
|
||||
last_name: null,
|
||||
_id: null,
|
||||
enrollment: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -66,6 +71,7 @@ describe('UserMembershipViewModel', function () {
|
|||
first_name: this.user.first_name,
|
||||
last_name: null,
|
||||
_id: this.user._id,
|
||||
enrollment: this.user.enrollment,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -107,6 +113,8 @@ describe('UserMembershipViewModel', function () {
|
|||
expect(viewModel.first_name).to.equal(this.user.first_name)
|
||||
expect(viewModel.invite).to.equal(false)
|
||||
expect(viewModel.email).to.exist
|
||||
expect(viewModel.enrollment).to.exist
|
||||
expect(viewModel.enrollment).to.deep.equal(this.user.enrollment)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
export type UserEnrollment = {
|
||||
managedBy: string
|
||||
enrolledAt: Date
|
||||
}
|
||||
|
||||
export type User = {
|
||||
_id: string
|
||||
email: string
|
||||
|
@ -5,4 +10,6 @@ export type User = {
|
|||
last_name: string
|
||||
invite: boolean
|
||||
last_active_at: Date
|
||||
enrollment: UserEnrollment | undefined
|
||||
isEntityAdmin: boolean | undefined
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue