mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-15 14:43:46 +01:00
Merge pull request #23806 from overleaf/ii-bs5-manage-group-managers
[web] BS5 Group managers management GitOrigin-RevId: 912fd29c3d5cf07eab0cd67e2771e60b1ba25fb3
This commit is contained in:
@@ -120,6 +120,8 @@ async function _renderManagersPage(req, res, next, template) {
|
|||||||
entityConfig
|
entityConfig
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await SplitTestHandler.promises.getAssignment(req, res, 'bootstrap-5-groups')
|
||||||
|
|
||||||
res.render(template, {
|
res.render(template, {
|
||||||
name: entityName,
|
name: entityName,
|
||||||
users,
|
users,
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ extends ../layout-marketing
|
|||||||
block entrypointVar
|
block entrypointVar
|
||||||
- entrypoint = 'pages/user/subscription/group-management/group-managers'
|
- entrypoint = 'pages/user/subscription/group-management/group-managers'
|
||||||
|
|
||||||
|
block vars
|
||||||
|
- bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly'
|
||||||
|
- bootstrap5PageSplitTest = 'bootstrap-5-groups'
|
||||||
|
|
||||||
block append meta
|
block append meta
|
||||||
meta(name="ol-users", data-type="json", content=users)
|
meta(name="ol-users", data-type="json", content=users)
|
||||||
meta(name="ol-groupId", data-type="string", content=groupId)
|
meta(name="ol-groupId", data-type="string", content=groupId)
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ extends ../layout-marketing
|
|||||||
block entrypointVar
|
block entrypointVar
|
||||||
- entrypoint = 'pages/user/subscription/group-management/institution-managers'
|
- entrypoint = 'pages/user/subscription/group-management/institution-managers'
|
||||||
|
|
||||||
|
block vars
|
||||||
|
- bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly'
|
||||||
|
- bootstrap5PageSplitTest = 'bootstrap-5-groups'
|
||||||
|
|
||||||
block append meta
|
block append meta
|
||||||
meta(name="ol-users", data-type="json", content=users)
|
meta(name="ol-users", data-type="json", content=users)
|
||||||
meta(name="ol-groupId", data-type="string", content=groupId)
|
meta(name="ol-groupId", data-type="string", content=groupId)
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ extends ../layout-marketing
|
|||||||
block entrypointVar
|
block entrypointVar
|
||||||
- entrypoint = 'pages/user/subscription/group-management/publisher-managers'
|
- entrypoint = 'pages/user/subscription/group-management/publisher-managers'
|
||||||
|
|
||||||
|
block vars
|
||||||
|
- bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly'
|
||||||
|
- bootstrap5PageSplitTest = 'bootstrap-5-groups'
|
||||||
|
|
||||||
block append meta
|
block append meta
|
||||||
meta(name="ol-users", data-type="json", content=users)
|
meta(name="ol-users", data-type="json", content=users)
|
||||||
meta(name="ol-groupId", data-type="string", content=groupId)
|
meta(name="ol-groupId", data-type="string", content=groupId)
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { Button, Col, Form, FormControl, Row } from 'react-bootstrap'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { deleteJSON, FetchError, postJSON } from '@/infrastructure/fetch-json'
|
import { deleteJSON, FetchError, postJSON } from '@/infrastructure/fetch-json'
|
||||||
import MaterialIcon from '../../../shared/components/material-icon'
|
|
||||||
import Tooltip from '../../../shared/components/tooltip'
|
|
||||||
import getMeta from '../../../utils/meta'
|
import getMeta from '../../../utils/meta'
|
||||||
import { parseEmails } from '../utils/emails'
|
import { parseEmails } from '../utils/emails'
|
||||||
import ErrorAlert, { APIError } from './error-alert'
|
import ErrorAlert, { APIError } from './error-alert'
|
||||||
@@ -11,6 +8,17 @@ import UserRow from './user-row'
|
|||||||
import useUserSelection from '../hooks/use-user-selection'
|
import useUserSelection from '../hooks/use-user-selection'
|
||||||
import { User } from '../../../../../types/group-management/user'
|
import { User } from '../../../../../types/group-management/user'
|
||||||
import { debugConsole } from '@/utils/debugging'
|
import { debugConsole } from '@/utils/debugging'
|
||||||
|
import OLRow from '@/features/ui/components/ol/ol-row'
|
||||||
|
import OLCol from '@/features/ui/components/ol/ol-col'
|
||||||
|
import BackButton from '@/features/group-management/components/back-button'
|
||||||
|
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'
|
||||||
|
import OLTable from '@/features/ui/components/ol/ol-table'
|
||||||
|
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||||
|
import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
|
||||||
|
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||||
|
|
||||||
type ManagersPaths = {
|
type ManagersPaths = {
|
||||||
addMember: string
|
addMember: string
|
||||||
@@ -132,136 +140,155 @@ export function ManagersTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<Row>
|
<OLRow>
|
||||||
<Col md={10} mdOffset={1}>
|
<OLCol lg={{ span: 10, offset: 1 }}>
|
||||||
<h1>
|
<div className="group-heading" data-testid="group-heading">
|
||||||
<a href="/user/subscription" className="back-btn">
|
<BackButton
|
||||||
<MaterialIcon
|
href="/user/subscription"
|
||||||
type="arrow_back"
|
accessibilityLabel={t('back_to_subscription')}
|
||||||
accessibilityLabel={t('back_to_subscription')}
|
/>
|
||||||
/>
|
<h1 className="heading">{groupName || translations.title}</h1>
|
||||||
</a>{' '}
|
</div>
|
||||||
{groupName || translations.title}
|
<OLCard>
|
||||||
</h1>
|
<div
|
||||||
<div className="card">
|
className="page-header mb-4"
|
||||||
<div className="page-header">
|
data-testid="page-header-members-details"
|
||||||
|
>
|
||||||
<div className="pull-right">
|
<div className="pull-right">
|
||||||
{removeMemberInflightCount > 0 ? (
|
{removeMemberInflightCount > 0 ? (
|
||||||
<Button bsStyle="danger" disabled>
|
<OLButton variant="danger" disabled>
|
||||||
{t('removing')}…
|
{t('removing')}…
|
||||||
</Button>
|
</OLButton>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{selectedUsers.length > 0 && (
|
{selectedUsers.length > 0 && (
|
||||||
<Button bsStyle="danger" onClick={removeManagers}>
|
<OLButton variant="danger" onClick={removeManagers}>
|
||||||
{translations.remove}
|
{translations.remove}
|
||||||
</Button>
|
</OLButton>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h3>{translations.subtitle}</h3>
|
<h2 className="h3 mt-0">{translations.subtitle}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="row-spaced-small">
|
<div className="row-spaced-small">
|
||||||
<ErrorAlert error={removeMemberError} />
|
<ErrorAlert error={removeMemberError} />
|
||||||
<ul className="list-unstyled structured-list">
|
<OLTable
|
||||||
<li className="container-fluid">
|
className="managed-users-table managed-users-list structured-list"
|
||||||
<Row>
|
data-testid="managed-users-table"
|
||||||
<Col xs={4}>
|
>
|
||||||
<label htmlFor="select-all" className="sr-only">
|
<thead>
|
||||||
{t('select_all')}
|
<tr>
|
||||||
</label>
|
<th className="cell-checkbox">
|
||||||
<input
|
<BootstrapVersionSwitcher
|
||||||
className="select-all"
|
bs3={
|
||||||
id="select-all"
|
<input
|
||||||
type="checkbox"
|
className="select-all"
|
||||||
autoComplete="off"
|
type="checkbox"
|
||||||
onChange={handleSelectAllClick}
|
autoComplete="off"
|
||||||
checked={selectedUsers.length === users.length}
|
onChange={handleSelectAllClick}
|
||||||
|
checked={selectedUsers.length === users.length}
|
||||||
|
aria-label={t('select_all')}
|
||||||
|
data-testid="select-all-checkbox"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
bs5={
|
||||||
|
<OLFormCheckbox
|
||||||
|
autoComplete="off"
|
||||||
|
onChange={handleSelectAllClick}
|
||||||
|
checked={selectedUsers.length === users.length}
|
||||||
|
aria-label={t('select_all')}
|
||||||
|
data-testid="select-all-checkbox"
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<span className="header">{t('email')}</span>
|
</th>
|
||||||
</Col>
|
<th>{t('email')}</th>
|
||||||
<Col xs={4}>
|
<th className="cell-name">{t('name')}</th>
|
||||||
<span className="header">{t('name')}</span>
|
<th className="cell-last-active">
|
||||||
</Col>
|
<OLTooltip
|
||||||
<Col xs={2}>
|
|
||||||
<Tooltip
|
|
||||||
id="last-active-tooltip"
|
id="last-active-tooltip"
|
||||||
description={t('last_active_description')}
|
description={t('last_active_description')}
|
||||||
overlayProps={{
|
overlayProps={{
|
||||||
placement: 'left',
|
placement: 'left',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="header">
|
<span>
|
||||||
{t('last_active')}
|
{t('last_active')}
|
||||||
<sup>(?)</sup>
|
<sup>(?)</sup>
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</OLTooltip>
|
||||||
</Col>
|
</th>
|
||||||
<Col xs={2}>
|
<th className="cell-accepted-invite">
|
||||||
<span className="header">{t('accepted_invite')}</span>
|
{t('accepted_invite')}
|
||||||
</Col>
|
</th>
|
||||||
</Row>
|
</tr>
|
||||||
</li>
|
</thead>
|
||||||
{users.length === 0 && (
|
<tbody>
|
||||||
<li>
|
{users.length === 0 && (
|
||||||
<Row>
|
<tr>
|
||||||
<Col md={12} className="text-centered">
|
<td className="text-center" colSpan={5}>
|
||||||
<small>{t('no_members')}</small>
|
<small>{t('no_members')}</small>
|
||||||
</Col>
|
</td>
|
||||||
</Row>
|
</tr>
|
||||||
</li>
|
)}
|
||||||
)}
|
{users.map(user => (
|
||||||
{users.map(user => (
|
<UserRow
|
||||||
<UserRow
|
key={user.email}
|
||||||
key={user.email}
|
user={user}
|
||||||
user={user}
|
selectUser={selectUser}
|
||||||
selectUser={selectUser}
|
unselectUser={unselectUser}
|
||||||
unselectUser={unselectUser}
|
selected={selectedUsers.includes(user)}
|
||||||
selected={selectedUsers.includes(user)}
|
/>
|
||||||
/>
|
))}
|
||||||
))}
|
</tbody>
|
||||||
</ul>
|
</OLTable>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div>
|
<div>
|
||||||
<p className="small">{t('add_more_managers')}</p>
|
<p className="small">{t('add_more_managers')}</p>
|
||||||
<ErrorAlert error={inviteError} />
|
<ErrorAlert error={inviteError} />
|
||||||
<Form horizontal onSubmit={addManagers} className="form">
|
<form onSubmit={addManagers} data-testid="add-members-form">
|
||||||
<Row>
|
<OLRow>
|
||||||
<Col xs={6}>
|
<OLCol xs={6}>
|
||||||
<FormControl
|
<OLFormControl
|
||||||
type="input"
|
type="input"
|
||||||
placeholder="jane@example.com, joe@example.com"
|
placeholder="jane@example.com, joe@example.com"
|
||||||
aria-describedby="add-members-description"
|
aria-describedby="add-members-description"
|
||||||
value={emailString}
|
value={emailString}
|
||||||
onChange={handleEmailsChange}
|
onChange={handleEmailsChange}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</OLCol>
|
||||||
<Col xs={4}>
|
<OLCol xs={4}>
|
||||||
{inviteUserInflightCount > 0 ? (
|
<OLButton
|
||||||
<Button bsStyle="primary" disabled>
|
variant="primary"
|
||||||
{t('adding')}…
|
onClick={addManagers}
|
||||||
</Button>
|
isLoading={inviteUserInflightCount > 0}
|
||||||
) : (
|
bs3Props={{
|
||||||
<Button bsStyle="primary" onClick={addManagers}>
|
loading:
|
||||||
{t('add')}
|
inviteUserInflightCount > 0 ? (
|
||||||
</Button>
|
<>{t('adding')}…</>
|
||||||
)}
|
) : (
|
||||||
</Col>
|
t('add')
|
||||||
</Row>
|
),
|
||||||
<Row>
|
}}
|
||||||
<Col xs={8}>
|
>
|
||||||
<span className="help-block">
|
{t('add')}
|
||||||
|
</OLButton>
|
||||||
|
</OLCol>
|
||||||
|
</OLRow>
|
||||||
|
<OLRow>
|
||||||
|
<OLCol xs={8}>
|
||||||
|
<OLFormText bs3Props={{ className: 'help-block' }}>
|
||||||
{t('add_comma_separated_emails_help')}
|
{t('add_comma_separated_emails_help')}
|
||||||
</span>
|
</OLFormText>
|
||||||
</Col>
|
</OLCol>
|
||||||
</Row>
|
</OLRow>
|
||||||
</Form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</OLCard>
|
||||||
</Col>
|
</OLCol>
|
||||||
</Row>
|
</OLRow>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { Col, Row } from 'react-bootstrap'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { User } from '../../../../../types/group-management/user'
|
import { User } from '../../../../../types/group-management/user'
|
||||||
|
import OLFormCheckbox from '@/features/ui/components/ol/ol-form-checkbox'
|
||||||
|
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||||
|
import Icon from '@/shared/components/icon'
|
||||||
|
import MaterialIcon from '@/shared/components/material-icon'
|
||||||
|
|
||||||
type GroupMemberRowProps = {
|
type GroupMemberRowProps = {
|
||||||
user: User
|
user: User
|
||||||
@@ -31,52 +34,75 @@ export default function UserRow({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={`user-${user.email}`}>
|
<tr key={`user-${user.email}`} className="managed-user-row">
|
||||||
<Row>
|
<td className="cell-checkbox">
|
||||||
<Col xs={4}>
|
<BootstrapVersionSwitcher
|
||||||
<label htmlFor={`select-user-${user.email}`} className="sr-only">
|
bs3={
|
||||||
{t('select_user')}
|
<input
|
||||||
</label>
|
className="select-item"
|
||||||
<input
|
type="checkbox"
|
||||||
className="select-item"
|
autoComplete="off"
|
||||||
id={`select-user-${user.email}`}
|
checked={selected}
|
||||||
type="checkbox"
|
onChange={e => handleSelectUser(e, user)}
|
||||||
autoComplete="off"
|
aria-label={t('select_user')}
|
||||||
checked={selected}
|
data-testid="select-single-checkbox"
|
||||||
onChange={e => handleSelectUser(e, user)}
|
/>
|
||||||
|
}
|
||||||
|
bs5={
|
||||||
|
<OLFormCheckbox
|
||||||
|
autoComplete="off"
|
||||||
|
checked={selected}
|
||||||
|
onChange={e => handleSelectUser(e, user)}
|
||||||
|
aria-label={t('select_user')}
|
||||||
|
data-testid="select-single-checkbox"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>{user.email}</td>
|
||||||
|
<td className="cell-name">
|
||||||
|
{user.first_name} {user.last_name}
|
||||||
|
</td>
|
||||||
|
<td className="cell-last-active">
|
||||||
|
{user.last_active_at
|
||||||
|
? moment(user.last_active_at).format('Do MMM YYYY')
|
||||||
|
: 'N/A'}
|
||||||
|
</td>
|
||||||
|
<td className="cell-accepted-invite">
|
||||||
|
{user.invite ? (
|
||||||
|
<BootstrapVersionSwitcher
|
||||||
|
bs3={
|
||||||
|
<Icon
|
||||||
|
type="times"
|
||||||
|
accessibilityLabel={t('invite_not_accepted')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
bs5={
|
||||||
|
<MaterialIcon
|
||||||
|
type="clear"
|
||||||
|
accessibilityLabel={t('invite_not_accepted')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<span>{user.email}</span>
|
) : (
|
||||||
</Col>
|
<BootstrapVersionSwitcher
|
||||||
<Col xs={4}>
|
bs3={
|
||||||
{user.first_name} {user.last_name}
|
<Icon
|
||||||
</Col>
|
type="check"
|
||||||
<Col xs={2}>
|
className="text-success"
|
||||||
{user.last_active_at
|
accessibilityLabel={t('accepted_invite')}
|
||||||
? moment(user.last_active_at).format('Do MMM YYYY')
|
|
||||||
: 'N/A'}
|
|
||||||
</Col>
|
|
||||||
<Col xs={2}>
|
|
||||||
{user.invite ? (
|
|
||||||
<>
|
|
||||||
<i
|
|
||||||
className="fa fa-times"
|
|
||||||
aria-hidden="true"
|
|
||||||
aria-label={t('invite_not_accepted')}
|
|
||||||
/>
|
/>
|
||||||
<span className="sr-only">{t('invite_not_accepted')}</span>
|
}
|
||||||
</>
|
bs5={
|
||||||
) : (
|
<MaterialIcon
|
||||||
<>
|
type="check"
|
||||||
<i
|
className="text-success"
|
||||||
className="fa fa-check text-success"
|
accessibilityLabel={t('accepted_invite')}
|
||||||
aria-hidden="true"
|
|
||||||
aria-label={t('accepted_invite')}
|
|
||||||
/>
|
/>
|
||||||
<span className="sr-only">{t('accepted_invite')}</span>
|
}
|
||||||
</>
|
/>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</td>
|
||||||
</Row>
|
</tr>
|
||||||
</li>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,10 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cell-accepted-invite {
|
||||||
|
width: 17%;
|
||||||
|
}
|
||||||
|
|
||||||
&.managed-users-active {
|
&.managed-users-active {
|
||||||
.cell-email {
|
.cell-email {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
@@ -131,6 +135,10 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cell-accepted-invite {
|
||||||
|
width: 17%;
|
||||||
|
}
|
||||||
|
|
||||||
&.managed-users-active {
|
&.managed-users-active {
|
||||||
.cell-email {
|
.cell-email {
|
||||||
width: 41%;
|
width: 41%;
|
||||||
|
|||||||
@@ -126,6 +126,10 @@
|
|||||||
min-width: 25px;
|
min-width: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cell-accepted-invite {
|
||||||
|
width: 17%;
|
||||||
|
}
|
||||||
|
|
||||||
&.managed-users-active {
|
&.managed-users-active {
|
||||||
.cell-email {
|
.cell-email {
|
||||||
width: 35%;
|
width: 35%;
|
||||||
@@ -185,6 +189,10 @@
|
|||||||
min-width: 25px;
|
min-width: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cell-accepted-invite {
|
||||||
|
width: 17%;
|
||||||
|
}
|
||||||
|
|
||||||
&.managed-users-active {
|
&.managed-users-active {
|
||||||
.cell-email {
|
.cell-email {
|
||||||
width: 42%;
|
width: 42%;
|
||||||
|
|||||||
+81
-59
@@ -35,23 +35,25 @@ describe('group managers', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders the group management page', function () {
|
it('renders the group management page', function () {
|
||||||
cy.get('h1').contains('My Awesome Team')
|
cy.findByRole('heading', { name: /my awesome team/i, level: 1 })
|
||||||
|
|
||||||
cy.get('ul').within(() => {
|
cy.findByTestId('managed-users-table')
|
||||||
cy.get('li:nth-child(2)').within(() => {
|
.find('tbody')
|
||||||
cy.contains('john.doe@test.com')
|
.within(() => {
|
||||||
cy.contains('John Doe')
|
cy.get('tr:nth-child(1)').within(() => {
|
||||||
cy.contains('15th Jan 2023')
|
cy.findByText('john.doe@test.com')
|
||||||
cy.get(`[aria-label="Invite not yet accepted"]`)
|
cy.findByText('John Doe')
|
||||||
})
|
cy.findByText('15th Jan 2023')
|
||||||
|
cy.findByText('Invite not yet accepted')
|
||||||
|
})
|
||||||
|
|
||||||
cy.get('li:nth-child(3)').within(() => {
|
cy.get('tr:nth-child(2)').within(() => {
|
||||||
cy.contains('bobby.lapointe@test.com')
|
cy.findByText('bobby.lapointe@test.com')
|
||||||
cy.contains('Bobby Lapointe')
|
cy.findByText('Bobby Lapointe')
|
||||||
cy.contains('2nd Jan 2023')
|
cy.findByText('2nd Jan 2023')
|
||||||
cy.get(`[aria-label="Accepted invite"]`)
|
cy.findByText('Accepted invite')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sends an invite', function () {
|
it('sends an invite', function () {
|
||||||
@@ -65,16 +67,20 @@ describe('group managers', function () {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get('.form-control').type('someone.else@test.com')
|
cy.findByTestId('add-members-form').within(() => {
|
||||||
cy.get('button').click()
|
cy.findByRole('textbox').type('someone.else@test.com')
|
||||||
|
cy.findByRole('button').click()
|
||||||
cy.get('ul').within(() => {
|
|
||||||
cy.get('li:nth-child(4)').within(() => {
|
|
||||||
cy.contains('someone.else@test.com')
|
|
||||||
cy.contains('N/A')
|
|
||||||
cy.get(`[aria-label="Invite not yet accepted"]`)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
cy.findByTestId('managed-users-table')
|
||||||
|
.find('tbody')
|
||||||
|
.within(() => {
|
||||||
|
cy.get('tr:nth-child(3)').within(() => {
|
||||||
|
cy.findByText('someone.else@test.com')
|
||||||
|
cy.findByText('N/A')
|
||||||
|
cy.findByText('Invite not yet accepted')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tries to send an invite and displays the error', function () {
|
it('tries to send an invite and displays the error', function () {
|
||||||
@@ -87,31 +93,41 @@ describe('group managers', function () {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get('.form-control').type('someone.else@test.com')
|
cy.findByTestId('add-members-form').within(() => {
|
||||||
cy.get('button').click()
|
cy.findByRole('textbox').type('someone.else@test.com')
|
||||||
cy.get('.alert').contains('Error: User already added')
|
cy.findByRole('button').click()
|
||||||
|
})
|
||||||
|
cy.findByRole('alert').should('contain.text', 'Error: User already added')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('checks the select all checkbox', function () {
|
it('checks the select all checkbox', function () {
|
||||||
cy.get('ul').within(() => {
|
cy.findByTestId('managed-users-table')
|
||||||
cy.get('li:nth-child(2)').within(() => {
|
.find('tbody')
|
||||||
cy.get('.select-item').should('not.be.checked')
|
.within(() => {
|
||||||
|
cy.get('tr:nth-child(1)').within(() => {
|
||||||
|
cy.findByLabelText(/select user/i).should('not.be.checked')
|
||||||
|
})
|
||||||
|
cy.get('tr:nth-child(2)').within(() => {
|
||||||
|
cy.findByLabelText(/select user/i).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.findByTestId('managed-users-table')
|
||||||
|
.find('thead')
|
||||||
|
.within(() => {
|
||||||
|
cy.findByLabelText(/select all/i).check()
|
||||||
|
})
|
||||||
|
|
||||||
cy.get('ul').within(() => {
|
cy.findByTestId('managed-users-table')
|
||||||
cy.get('li:nth-child(2)').within(() => {
|
.find('tbody')
|
||||||
cy.get('.select-item').should('be.checked')
|
.within(() => {
|
||||||
|
cy.get('tr:nth-child(1)').within(() => {
|
||||||
|
cy.findByLabelText(/select user/i).should('be.checked')
|
||||||
|
})
|
||||||
|
cy.get('tr:nth-child(2)').within(() => {
|
||||||
|
cy.findByLabelText(/select user/i).should('be.checked')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
cy.get('li:nth-child(3)').within(() => {
|
|
||||||
cy.get('.select-item').should('be.checked')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('remove a member', function () {
|
it('remove a member', function () {
|
||||||
@@ -119,22 +135,26 @@ describe('group managers', function () {
|
|||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get('ul').within(() => {
|
cy.findByTestId('managed-users-table')
|
||||||
cy.get('li:nth-child(2)').within(() => {
|
.find('tbody')
|
||||||
cy.get('.select-item').check()
|
.within(() => {
|
||||||
|
cy.get('tr:nth-child(1)').within(() => {
|
||||||
|
cy.findByLabelText(/select user/i).check()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
cy.get('button').contains('Remove manager').click()
|
cy.findByRole('button', { name: /remove manager/i }).click()
|
||||||
|
|
||||||
cy.get('ul').within(() => {
|
cy.findByTestId('managed-users-table')
|
||||||
cy.get('li:nth-child(2)').within(() => {
|
.find('tbody')
|
||||||
cy.contains('bobby.lapointe@test.com')
|
.within(() => {
|
||||||
cy.contains('Bobby Lapointe')
|
cy.get('tr:nth-child(1)').within(() => {
|
||||||
cy.contains('2nd Jan 2023')
|
cy.findByText('bobby.lapointe@test.com')
|
||||||
cy.get(`[aria-label="Accepted invite"]`)
|
cy.findByText('Bobby Lapointe')
|
||||||
|
cy.findByText('2nd Jan 2023')
|
||||||
|
cy.findByText('Accepted invite')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tries to remove a manager and displays the error', function () {
|
it('tries to remove a manager and displays the error', function () {
|
||||||
@@ -142,13 +162,15 @@ describe('group managers', function () {
|
|||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get('ul').within(() => {
|
cy.findByTestId('managed-users-table')
|
||||||
cy.get('li:nth-child(2)').within(() => {
|
.find('tbody')
|
||||||
cy.get('.select-item').check()
|
.within(() => {
|
||||||
|
cy.get('tr:nth-child(1)').within(() => {
|
||||||
|
cy.findByLabelText(/select user/i).check()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
cy.findByRole('button', { name: /remove manager/i }).click()
|
||||||
cy.get('button').contains('Remove manager').click()
|
|
||||||
|
|
||||||
cy.get('.alert').contains('Sorry, something went wrong')
|
cy.findByRole('alert').should('contain.text', 'Sorry, something went wrong')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+81
-59
@@ -35,23 +35,25 @@ describe('institution managers', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders the institution management page', function () {
|
it('renders the institution management page', function () {
|
||||||
cy.get('h1').contains('My Awesome Institution')
|
cy.findByRole('heading', { name: /my awesome institution/i, level: 1 })
|
||||||
|
|
||||||
cy.get('ul').within(() => {
|
cy.findByTestId('managed-users-table')
|
||||||
cy.get('li:nth-child(2)').within(() => {
|
.find('tbody')
|
||||||
cy.contains('john.doe@test.com')
|
.within(() => {
|
||||||
cy.contains('John Doe')
|
cy.get('tr:nth-child(1)').within(() => {
|
||||||
cy.contains('15th Jan 2023')
|
cy.findByText('john.doe@test.com')
|
||||||
cy.get(`[aria-label="Invite not yet accepted"]`)
|
cy.findByText('John Doe')
|
||||||
})
|
cy.findByText('15th Jan 2023')
|
||||||
|
cy.findByText('Invite not yet accepted')
|
||||||
|
})
|
||||||
|
|
||||||
cy.get('li:nth-child(3)').within(() => {
|
cy.get('tr:nth-child(2)').within(() => {
|
||||||
cy.contains('bobby.lapointe@test.com')
|
cy.findByText('bobby.lapointe@test.com')
|
||||||
cy.contains('Bobby Lapointe')
|
cy.findByText('Bobby Lapointe')
|
||||||
cy.contains('2nd Jan 2023')
|
cy.findByText('2nd Jan 2023')
|
||||||
cy.get(`[aria-label="Accepted invite"]`)
|
cy.findByText('Accepted invite')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sends an invite', function () {
|
it('sends an invite', function () {
|
||||||
@@ -65,16 +67,20 @@ describe('institution managers', function () {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get('.form-control').type('someone.else@test.com')
|
cy.findByTestId('add-members-form').within(() => {
|
||||||
cy.get('button').click()
|
cy.findByRole('textbox').type('someone.else@test.com')
|
||||||
|
cy.findByRole('button').click()
|
||||||
cy.get('ul').within(() => {
|
|
||||||
cy.get('li:nth-child(4)').within(() => {
|
|
||||||
cy.contains('someone.else@test.com')
|
|
||||||
cy.contains('N/A')
|
|
||||||
cy.get(`[aria-label="Invite not yet accepted"]`)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
cy.findByTestId('managed-users-table')
|
||||||
|
.find('tbody')
|
||||||
|
.within(() => {
|
||||||
|
cy.get('tr:nth-child(3)').within(() => {
|
||||||
|
cy.findByText('someone.else@test.com')
|
||||||
|
cy.findByText('N/A')
|
||||||
|
cy.findByText('Invite not yet accepted')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tries to send an invite and displays the error', function () {
|
it('tries to send an invite and displays the error', function () {
|
||||||
@@ -87,31 +93,41 @@ describe('institution managers', function () {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get('.form-control').type('someone.else@test.com')
|
cy.findByTestId('add-members-form').within(() => {
|
||||||
cy.get('button').click()
|
cy.findByRole('textbox').type('someone.else@test.com')
|
||||||
cy.get('.alert').contains('Error: User already added')
|
cy.findByRole('button').click()
|
||||||
|
})
|
||||||
|
cy.findByRole('alert').should('contain.text', 'Error: User already added')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('checks the select all checkbox', function () {
|
it('checks the select all checkbox', function () {
|
||||||
cy.get('ul').within(() => {
|
cy.findByTestId('managed-users-table')
|
||||||
cy.get('li:nth-child(2)').within(() => {
|
.find('tbody')
|
||||||
cy.get('.select-item').should('not.be.checked')
|
.within(() => {
|
||||||
|
cy.get('tr:nth-child(1)').within(() => {
|
||||||
|
cy.findByLabelText(/select user/i).should('not.be.checked')
|
||||||
|
})
|
||||||
|
cy.get('tr:nth-child(2)').within(() => {
|
||||||
|
cy.findByLabelText(/select user/i).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.findByTestId('managed-users-table')
|
||||||
|
.find('thead')
|
||||||
|
.within(() => {
|
||||||
|
cy.findByLabelText(/select all/i).check()
|
||||||
|
})
|
||||||
|
|
||||||
cy.get('ul').within(() => {
|
cy.findByTestId('managed-users-table')
|
||||||
cy.get('li:nth-child(2)').within(() => {
|
.find('tbody')
|
||||||
cy.get('.select-item').should('be.checked')
|
.within(() => {
|
||||||
|
cy.get('tr:nth-child(1)').within(() => {
|
||||||
|
cy.findByLabelText(/select user/i).should('be.checked')
|
||||||
|
})
|
||||||
|
cy.get('tr:nth-child(2)').within(() => {
|
||||||
|
cy.findByLabelText(/select user/i).should('be.checked')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
cy.get('li:nth-child(3)').within(() => {
|
|
||||||
cy.get('.select-item').should('be.checked')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('remove a member', function () {
|
it('remove a member', function () {
|
||||||
@@ -119,22 +135,26 @@ describe('institution managers', function () {
|
|||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get('ul').within(() => {
|
cy.findByTestId('managed-users-table')
|
||||||
cy.get('li:nth-child(2)').within(() => {
|
.find('tbody')
|
||||||
cy.get('.select-item').check()
|
.within(() => {
|
||||||
|
cy.get('tr:nth-child(1)').within(() => {
|
||||||
|
cy.findByLabelText(/select user/i).check()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
cy.get('button').contains('Remove manager').click()
|
cy.findByRole('button', { name: /remove manager/i }).click()
|
||||||
|
|
||||||
cy.get('ul').within(() => {
|
cy.findByTestId('managed-users-table')
|
||||||
cy.get('li:nth-child(2)').within(() => {
|
.find('tbody')
|
||||||
cy.contains('bobby.lapointe@test.com')
|
.within(() => {
|
||||||
cy.contains('Bobby Lapointe')
|
cy.get('tr:nth-child(1)').within(() => {
|
||||||
cy.contains('2nd Jan 2023')
|
cy.findByText('bobby.lapointe@test.com')
|
||||||
cy.get(`[aria-label="Accepted invite"]`)
|
cy.findByText('Bobby Lapointe')
|
||||||
|
cy.findByText('2nd Jan 2023')
|
||||||
|
cy.findByText('Accepted invite')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tries to remove a manager and displays the error', function () {
|
it('tries to remove a manager and displays the error', function () {
|
||||||
@@ -142,13 +162,15 @@ describe('institution managers', function () {
|
|||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get('ul').within(() => {
|
cy.findByTestId('managed-users-table')
|
||||||
cy.get('li:nth-child(2)').within(() => {
|
.find('tbody')
|
||||||
cy.get('.select-item').check()
|
.within(() => {
|
||||||
|
cy.get('tr:nth-child(1)').within(() => {
|
||||||
|
cy.findByLabelText(/select user/i).check()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
cy.findByRole('button', { name: /remove manager/i }).click()
|
||||||
cy.get('button').contains('Remove manager').click()
|
|
||||||
|
|
||||||
cy.get('.alert').contains('Sorry, something went wrong')
|
cy.findByRole('alert').should('contain.text', 'Sorry, something went wrong')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+76
-58
@@ -35,23 +35,25 @@ describe('publisher managers', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders the publisher management page', function () {
|
it('renders the publisher management page', function () {
|
||||||
cy.get('h1').contains('My Awesome Publisher')
|
cy.findByRole('heading', { name: /my awesome publisher/i, level: 1 })
|
||||||
|
|
||||||
cy.get('ul').within(() => {
|
cy.findByTestId('managed-users-table')
|
||||||
cy.get('li:nth-child(2)').within(() => {
|
.find('tbody')
|
||||||
cy.contains('john.doe@test.com')
|
.within(() => {
|
||||||
cy.contains('John Doe')
|
cy.get('tr:nth-child(1)').within(() => {
|
||||||
cy.contains('15th Jan 2023')
|
cy.findByText('john.doe@test.com')
|
||||||
cy.get(`[aria-label="Invite not yet accepted"]`)
|
cy.findByText('John Doe')
|
||||||
})
|
cy.findByText('15th Jan 2023')
|
||||||
|
cy.findByText('Invite not yet accepted')
|
||||||
|
})
|
||||||
|
|
||||||
cy.get('li:nth-child(3)').within(() => {
|
cy.get('tr:nth-child(2)').within(() => {
|
||||||
cy.contains('bobby.lapointe@test.com')
|
cy.findByText('bobby.lapointe@test.com')
|
||||||
cy.contains('Bobby Lapointe')
|
cy.findByText('Bobby Lapointe')
|
||||||
cy.contains('2nd Jan 2023')
|
cy.findByText('2nd Jan 2023')
|
||||||
cy.get(`[aria-label="Accepted invite"]`)
|
cy.findByText('Accepted invite')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sends an invite', function () {
|
it('sends an invite', function () {
|
||||||
@@ -65,16 +67,20 @@ describe('publisher managers', function () {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get('.form-control').type('someone.else@test.com')
|
cy.findByTestId('add-members-form').within(() => {
|
||||||
cy.get('button').click()
|
cy.findByRole('textbox').type('someone.else@test.com')
|
||||||
|
cy.findByRole('button').click()
|
||||||
cy.get('ul').within(() => {
|
|
||||||
cy.get('li:nth-child(4)').within(() => {
|
|
||||||
cy.contains('someone.else@test.com')
|
|
||||||
cy.contains('N/A')
|
|
||||||
cy.get(`[aria-label="Invite not yet accepted"]`)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
cy.findByTestId('managed-users-table')
|
||||||
|
.find('tbody')
|
||||||
|
.within(() => {
|
||||||
|
cy.get('tr:nth-child(3)').within(() => {
|
||||||
|
cy.findByText('someone.else@test.com')
|
||||||
|
cy.findByText('N/A')
|
||||||
|
cy.findByText('Invite not yet accepted')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tries to send an invite and displays the error', function () {
|
it('tries to send an invite and displays the error', function () {
|
||||||
@@ -87,31 +93,37 @@ describe('publisher managers', function () {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get('.form-control').type('someone.else@test.com')
|
cy.findByTestId('add-members-form').within(() => {
|
||||||
cy.get('button').click()
|
cy.findByRole('textbox').type('someone.else@test.com')
|
||||||
cy.get('.alert').contains('Error: User already added')
|
cy.findByRole('button').click()
|
||||||
|
})
|
||||||
|
cy.findByRole('alert').should('contain.text', 'Error: User already added')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('checks the select all checkbox', function () {
|
it('checks the select all checkbox', function () {
|
||||||
cy.get('ul').within(() => {
|
cy.findByTestId('managed-users-table')
|
||||||
cy.get('li:nth-child(2)').within(() => {
|
.find('tbody')
|
||||||
cy.get('.select-item').should('not.be.checked')
|
.within(() => {
|
||||||
|
cy.get('tr:nth-child(1)').within(() => {
|
||||||
|
cy.findByLabelText(/select user/i).should('not.be.checked')
|
||||||
|
})
|
||||||
|
cy.get('tr:nth-child(2)').within(() => {
|
||||||
|
cy.findByLabelText(/select user/i).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('.select-all').click()
|
||||||
|
|
||||||
cy.get('ul').within(() => {
|
cy.findByTestId('managed-users-table')
|
||||||
cy.get('li:nth-child(2)').within(() => {
|
.find('tbody')
|
||||||
cy.get('.select-item').should('be.checked')
|
.within(() => {
|
||||||
|
cy.get('tr:nth-child(1)').within(() => {
|
||||||
|
cy.findByLabelText(/select user/i).should('be.checked')
|
||||||
|
})
|
||||||
|
cy.get('tr:nth-child(2)').within(() => {
|
||||||
|
cy.findByLabelText(/select user/i).should('be.checked')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
cy.get('li:nth-child(3)').within(() => {
|
|
||||||
cy.get('.select-item').should('be.checked')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('remove a member', function () {
|
it('remove a member', function () {
|
||||||
@@ -119,22 +131,26 @@ describe('publisher managers', function () {
|
|||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get('ul').within(() => {
|
cy.findByTestId('managed-users-table')
|
||||||
cy.get('li:nth-child(2)').within(() => {
|
.find('tbody')
|
||||||
cy.get('.select-item').check()
|
.within(() => {
|
||||||
|
cy.get('tr:nth-child(1)').within(() => {
|
||||||
|
cy.findByLabelText(/select user/i).check()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
cy.get('button').contains('Remove manager').click()
|
cy.findByRole('button', { name: 'Remove manager' }).click()
|
||||||
|
|
||||||
cy.get('ul').within(() => {
|
cy.findByTestId('managed-users-table')
|
||||||
cy.get('li:nth-child(2)').within(() => {
|
.find('tbody')
|
||||||
cy.contains('bobby.lapointe@test.com')
|
.within(() => {
|
||||||
cy.contains('Bobby Lapointe')
|
cy.get('tr:nth-child(1)').within(() => {
|
||||||
cy.contains('2nd Jan 2023')
|
cy.findByText('bobby.lapointe@test.com')
|
||||||
cy.get(`[aria-label="Accepted invite"]`)
|
cy.findByText('Bobby Lapointe')
|
||||||
|
cy.findByText('2nd Jan 2023')
|
||||||
|
cy.findByText('Accepted invite')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tries to remove a manager and displays the error', function () {
|
it('tries to remove a manager and displays the error', function () {
|
||||||
@@ -142,13 +158,15 @@ describe('publisher managers', function () {
|
|||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
})
|
})
|
||||||
|
|
||||||
cy.get('ul').within(() => {
|
cy.findByTestId('managed-users-table')
|
||||||
cy.get('li:nth-child(2)').within(() => {
|
.find('tbody')
|
||||||
cy.get('.select-item').check()
|
.within(() => {
|
||||||
|
cy.get('tr:nth-child(1)').within(() => {
|
||||||
|
cy.findByLabelText(/select user/i).check()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
cy.findByRole('button', { name: /remove manager/i }).click()
|
||||||
cy.get('button').contains('Remove manager').click()
|
|
||||||
|
|
||||||
cy.get('.alert').contains('Sorry, something went wrong')
|
cy.findByRole('alert').should('contain.text', 'Sorry, something went wrong')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user