Просмотр исходного кода

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
Alexandre Bourdin 3 лет назад
Родитель
Сommit
0c5ba1e96e

+ 5 - 2
services/web/frontend/fonts/material-symbols.css

@@ -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;
+}

+ 68 - 71
services/web/frontend/js/features/group-management/components/managed-users/managed-user-row.tsx

@@ -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>
   )
 }

+ 11 - 12
services/web/frontend/js/features/group-management/components/managed-users/managed-user-status.tsx

@@ -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')}

+ 59 - 50
services/web/frontend/js/features/group-management/components/managed-users/managed-users-list.tsx

@@ -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

+ 1 - 1
services/web/frontend/js/features/history/components/change-list/dropdown/compare-version-dropdown.tsx

@@ -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"
         />
       }

+ 2 - 2
services/web/frontend/js/features/history/components/change-list/dropdown/menu-item/compare-items.tsx

@@ -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"
             />
           }
         />

+ 8 - 1
services/web/frontend/js/shared/components/material-icon.tsx

@@ -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 (
     <>

+ 115 - 0
services/web/frontend/stylesheets/components/group-members.less

@@ -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;
+  }
+}

+ 0 - 4
services/web/frontend/stylesheets/modules/managed-users.less

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

+ 27 - 27
services/web/test/frontend/features/group-management/components/managed-users/managed-group-members.spec.tsx

@@ -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()
       })
     })

+ 6 - 6
services/web/test/frontend/features/group-management/components/managed-users/managed-user-row.spec.tsx

@@ -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'