Merge pull request #14491 from overleaf/ab-update-managed-users-icons

[web] Update managed users icons and improve display on smaller screen sizes

GitOrigin-RevId: 7b6263ea9afa9bb52bed3a3f50cbe361e7064085
This commit is contained in:
Alexandre Bourdin 2023-08-25 10:51:45 +02:00 committed by Copybot
parent 43916c425b
commit 0c5ba1e96e
11 changed files with 302 additions and 176 deletions

View file

@ -7,12 +7,11 @@
format('woff2');
}
.material-symbols-rounded {
.material-symbols {
font-family: 'Material Symbols Rounded';
font-weight: normal;
font-style: normal;
font-size: 20px;
font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 20;
line-height: 1;
letter-spacing: normal;
text-transform: none;
@ -23,3 +22,7 @@
font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}
.material-symbols.material-symbols-rounded {
font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 20;
}

View file

@ -1,6 +1,5 @@
import moment from 'moment'
import { type Dispatch, type SetStateAction, 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'
@ -40,78 +39,76 @@ export default function ManagedUserRow({
const selected = selectedUsers.includes(user)
return (
<li
<tr
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>
&nbsp;
<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>
&nbsp;
<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}>
<span>
{user.first_name} {user.last_name}
</span>
</Col>
<Col xs={2}>
{user.last_active_at
? moment(user.last_active_at).format('Do MMM YYYY')
: 'N/A'}
</Col>
<Col xs={2}>
<div className="managed-user-security">
<ManagedUserStatus user={user} />
<ManagedUserDropdownButton
user={user}
openOffboardingModalForUser={openOffboardingModalForUser}
setManagedUserAlert={setManagedUserAlert}
groupId={groupId}
/>
</div>
</Col>
</Row>
</li>
<td className="cell-email">
<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>
&nbsp;
<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>
&nbsp;
<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>
</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-security">
<div className="managed-user-security">
<ManagedUserStatus user={user} />
</div>
</td>
<td className="cell-dropdown">
<ManagedUserDropdownButton
user={user}
openOffboardingModalForUser={openOffboardingModalForUser}
setManagedUserAlert={setManagedUserAlert}
groupId={groupId}
/>
</td>
</tr>
)
}

View file

@ -1,5 +1,6 @@
import { useTranslation } from 'react-i18next'
import { User } from '../../../../../../types/group-management/user'
import MaterialIcon from '../../../../shared/components/material-icon'
type ManagedUserStatusProps = {
user: User
@ -16,10 +17,10 @@ export default function ManagedUserStatus({ user }: ManagedUserStatusProps) {
<>
{user.invite ? (
<span className="security-state-invite-pending">
<i
className="fa fa-clock-o"
aria-hidden="true"
aria-label={t('pending_invite')}
<MaterialIcon
type="schedule"
category="outlined"
accessibilityLabel={t('pending_invite')}
/>
&nbsp;
{t('managed')}
@ -28,20 +29,18 @@ export default function ManagedUserStatus({ user }: ManagedUserStatusProps) {
<>
{user.enrollment?.managedBy ? (
<span className="security-state-managed">
<i
className="fa fa-check"
aria-hidden="true"
aria-label={t('managed')}
<MaterialIcon
type="check"
accessibilityLabel={t('managed')}
/>
&nbsp;
{t('managed')}
</span>
) : (
<span className="security-state-not-managed">
<i
className="fa fa-times"
aria-hidden="true"
aria-label={t('not_managed')}
<MaterialIcon
type="close"
accessibilityLabel={t('not_managed')}
/>
&nbsp;
{t('managed')}

View file

@ -38,59 +38,68 @@ export default function ManagedUsersList({
<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 xs={12}>
<table className="managed-users-table">
<thead>
<tr>
<td className="cell-email">
<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>
</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>
<td className="cell-security">
<span className="header">{t('security')}</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) => (
<ManagedUserRow
key={user.email}
user={user}
openOffboardingModalForUser={setUserToOffboard}
setManagedUserAlert={setManagedUserAlert}
groupId={groupId}
/>
))}
</tbody>
</table>
</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}
openOffboardingModalForUser={setUserToOffboard}
setManagedUserAlert={setManagedUserAlert}
groupId={groupId}
/>
))}
</ul>
{userToOffboard && (
<OffboardManagedUserModal

View file

@ -26,7 +26,7 @@ function CompareVersionDropdown({
iconTag={
<MaterialIcon
type="align_space_even"
className="material-symbols-rounded history-dropdown-icon p-1"
className="history-dropdown-icon p-1"
accessibilityLabel="compare drop down"
/>
}

View file

@ -41,7 +41,7 @@ function CompareItems({
icon={
<MaterialIcon
type="align_end"
className="material-symbols-rounded history-dropdown-icon p-1"
className="history-dropdown-icon p-1"
/>
}
/>
@ -60,7 +60,7 @@ function CompareItems({
icon={
<MaterialIcon
type="align_start"
className="material-symbols-rounded history-dropdown-icon p-1"
className="history-dropdown-icon p-1"
/>
}
/>

View file

@ -1,7 +1,9 @@
import classNames from 'classnames'
import React from 'react'
type IconOwnProps = {
type: string
category?: 'rounded' | 'outlined'
accessibilityLabel?: string
}
@ -10,11 +12,16 @@ type IconProps = IconOwnProps &
function MaterialIcon({
type,
category = 'rounded',
className,
accessibilityLabel,
...rest
}: IconProps) {
const iconClassName = classNames('material-symbols-rounded', className)
const iconClassName = classNames(
'material-symbols',
`material-symbols-${category}`,
className
)
return (
<>

View file

@ -73,3 +73,118 @@
}
}
}
.managed-users-table {
width: 100%;
table-layout: fixed;
tr {
border-bottom: 1px solid @structured-list-border-color;
}
th,
td {
padding: (@line-height-computed / 4) @line-height-computed / 2;
vertical-align: top;
}
thead {
tr:hover {
background-color: transparent;
}
}
tbody {
tr:last-child {
border-bottom: 0 none;
}
tr:hover {
background-color: #f6f7f9;
}
}
.cell-email {
width: 50%;
}
.cell-name {
width: 15%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cell-last-active {
width: 15%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cell-security {
width: 15%;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cell-dropdown {
width: 5%;
min-width: 25px;
}
@media (min-width: @screen-xs) {
.cell-email {
width: 40%;
}
.cell-name {
width: 20%;
}
.cell-last-active {
width: 20%;
}
.cell-security {
width: 15%;
}
.cell-dropdown {
width: 5%;
button.dropdown-toggle {
padding-left: 0;
padding-right: 0;
}
}
}
@media (min-width: @screen-lg) {
.cell-email {
width: 50%;
}
.cell-name {
width: 20%;
}
.cell-last-active {
width: 15%;
}
.cell-security {
width: 12%;
}
.cell-dropdown {
width: 3%;
}
}
}
.managed-user-security {
.material-symbols {
position: relative;
top: 4px;
}
}

View file

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

View file

@ -63,31 +63,31 @@ 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').within(() => {
cy.get('> li:nth-child(2)').within(() => {
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(`[aria-label="Pending invite"]`)
cy.get('.sr-only').contains('Pending invite')
cy.get('.badge-new-comment').contains('Pending invite')
cy.get(`.security-state-invite-pending`).should('exist')
})
cy.get('> li:nth-child(3)').within(() => {
cy.get('tr:nth-child(2)').within(() => {
cy.contains('bobby.lapointe@test.com')
cy.contains('Bobby Lapointe')
cy.contains('2nd Jan 2023')
cy.get('.badge-new-comment').should('not.exist')
cy.get('i[aria-label="Not managed"]').should('exist')
cy.get('.sr-only').contains('Not managed')
})
cy.get('> li:nth-child(4)').within(() => {
cy.get('tr:nth-child(3)').within(() => {
cy.contains('claire.jennings@test.com')
cy.contains('Claire Jennings')
cy.contains('3rd Jan 2023')
cy.get('.badge-new-comment').should('not.exist')
cy.get('i[aria-label="Managed"]').should('exist')
cy.get('.sr-only').contains('Managed')
})
})
})
@ -106,11 +106,11 @@ 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').within(() => {
cy.get('> li:nth-child(5)').within(() => {
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(`[aria-label="Pending invite"]`)
cy.get('.sr-only').contains('Pending invite')
cy.get('.badge-new-comment').contains('Pending invite')
cy.get(`.security-state-invite-pending`).should('exist')
})
@ -133,22 +133,22 @@ describe('group members, with managed users', function () {
})
it('checks the select all checkbox', function () {
cy.get('ul.managed-users-list').within(() => {
cy.get('> li:nth-child(2)').within(() => {
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.get('.select-item').should('not.be.checked')
})
cy.get('li:nth-child(3)').within(() => {
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').within(() => {
cy.get('> li:nth-child(2)').within(() => {
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.get('.select-item').should('be.checked')
})
cy.get('li:nth-child(3)').within(() => {
cy.get('tr:nth-child(2)').within(() => {
cy.get('.select-item').should('be.checked')
})
})
@ -159,8 +159,8 @@ describe('group members, with managed users', function () {
statusCode: 200,
})
cy.get('ul.managed-users-list').within(() => {
cy.get('> li:nth-child(2)').within(() => {
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.get('.select-item').check()
})
})
@ -168,8 +168,8 @@ describe('group members, with managed users', function () {
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.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')
@ -182,9 +182,9 @@ describe('group members, with managed users', function () {
statusCode: 200,
})
cy.get('ul.managed-users-list').within(() => {
cy.get('ul.managed-users-list table > tbody').within(() => {
// Select 'Claire Jennings', a managed user
cy.get('> li:nth-child(4)').within(() => {
cy.get('tr:nth-child(3)').within(() => {
cy.get('.select-item').check()
})
})
@ -199,13 +199,13 @@ describe('group members, with managed users', function () {
statusCode: 200,
})
cy.get('ul.managed-users-list').within(() => {
cy.get('ul.managed-users-list table > tbody').within(() => {
// Select 'Claire Jennings', a managed user
cy.get('> li:nth-child(4)').within(() => {
cy.get('tr:nth-child(3)').within(() => {
cy.get('.select-item').check()
})
// Select another user
cy.get('li:nth-child(3)').within(() => {
cy.get('tr:nth-child(2)').within(() => {
cy.get('.select-item').check()
})
})
@ -220,8 +220,8 @@ describe('group members, with managed users', function () {
statusCode: 500,
})
cy.get('ul.managed-users-list').within(() => {
cy.get('> li:nth-child(2)').within(() => {
cy.get('ul.managed-users-list table > tbody').within(() => {
cy.get('tr:nth-child(1)').within(() => {
cy.get('.select-item').check()
})
})

View file

@ -37,18 +37,18 @@ describe('ManagedUserRow', function () {
})
it('renders the row', function () {
cy.get('.row').should('exist')
cy.get('tr').should('exist')
// Checkbox
cy.get('.select-item').should('not.be.checked')
// Email
cy.get('.row').contains(user.email)
cy.get('tr').contains(user.email)
// Name
cy.get('.row').contains(user.first_name)
cy.get('.row').contains(user.last_name)
cy.get('tr').contains(user.first_name)
cy.get('tr').contains(user.last_name)
// Last active date
cy.get('.row').contains('21st Nov 2070')
cy.get('tr').contains('21st Nov 2070')
// Managed status
cy.get('.row').contains('Managed')
cy.get('tr').contains('Managed')
// Dropdown button
cy.get('#managed-user-dropdown-some\\.user\\@example\\.com').should(
'exist'