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

Merge pull request #15348 from overleaf/ab-decoupling-refactor-members-table

[web] Modular members management table refactoring

GitOrigin-RevId: 9a3a00a32970e78e5b43b3a68621a627c490c728
Alexandre Bourdin 3 лет назад
Родитель
Сommit
665f5e37c4
28 измененных файлов с 1203 добавлено и 537 удалено
  1. 2 2
      services/web/frontend/extracted-translations.json
  2. 0 89
      services/web/frontend/js/features/group-management/components/group-members-list.tsx
  3. 2 8
      services/web/frontend/js/features/group-management/components/group-members.tsx
  4. 3 7
      services/web/frontend/js/features/group-management/components/managers-table.tsx
  5. 4 7
      services/web/frontend/js/features/group-management/components/members-table/dropdown-button.tsx
  6. 1 1
      services/web/frontend/js/features/group-management/components/members-table/list-alert.tsx
  7. 1 1
      services/web/frontend/js/features/group-management/components/members-table/managed-user-status.tsx
  8. 14 11
      services/web/frontend/js/features/group-management/components/members-table/member-row.tsx
  9. 28 20
      services/web/frontend/js/features/group-management/components/members-table/members-list.tsx
  10. 5 5
      services/web/frontend/js/features/group-management/components/members-table/offboard-managed-user-modal.tsx
  11. 3 9
      services/web/frontend/js/features/group-management/components/members-table/select-all-checkbox.tsx
  12. 2 8
      services/web/frontend/js/features/group-management/components/members-table/select-user-checkbox.tsx
  13. 2 2
      services/web/frontend/js/features/group-management/components/members-table/sso-status.tsx
  14. 1 1
      services/web/frontend/js/features/group-management/components/user-row.tsx
  15. 3 7
      services/web/frontend/js/features/group-management/context/group-members-context.tsx
  16. 36 64
      services/web/frontend/stylesheets/components/group-members.less
  17. 2 2
      services/web/locales/en.json
  18. 1 1
      services/web/test/frontend/features/group-management/components/group-managers.spec.tsx
  19. 386 105
      services/web/test/frontend/features/group-management/components/group-members.spec.tsx
  20. 1 1
      services/web/test/frontend/features/group-management/components/institution-managers.spec.tsx
  21. 8 7
      services/web/test/frontend/features/group-management/components/managed-group-members.spec.tsx
  22. 0 167
      services/web/test/frontend/features/group-management/components/managed-users/managed-user-row.spec.tsx
  23. 4 3
      services/web/test/frontend/features/group-management/components/members-table/dropdown-button.spec.tsx
  24. 2 2
      services/web/test/frontend/features/group-management/components/members-table/managed-user-status.spec.tsx
  25. 685 0
      services/web/test/frontend/features/group-management/components/members-table/member-row.spec.tsx
  26. 5 5
      services/web/test/frontend/features/group-management/components/members-table/members-list.spec.tsx
  27. 1 1
      services/web/test/frontend/features/group-management/components/members-table/offboard-managed-user-modal.spec.tsx
  28. 1 1
      services/web/test/frontend/features/group-management/components/publisher-managers.spec.tsx

+ 2 - 2
services/web/frontend/extracted-translations.json

@@ -1074,6 +1074,7 @@
   "source": "",
   "spell_check": "",
   "sso": "",
+  "sso_active": "",
   "sso_config_prop_help_certificate": "",
   "sso_config_prop_help_first_name": "",
   "sso_config_prop_help_last_name": "",
@@ -1092,9 +1093,8 @@
   "sso_is_enabled_explanation_2": "",
   "sso_link_error": "",
   "sso_link_invite_has_been_sent_to_email": "",
-  "sso_linked": "",
   "sso_logs": "",
-  "sso_unlinked": "",
+  "sso_not_active": "",
   "start_a_free_trial": "",
   "start_by_adding_your_email": "",
   "start_free_trial": "",

+ 0 - 89
services/web/frontend/js/features/group-management/components/group-members-list.tsx

@@ -1,89 +0,0 @@
-import { useCallback } from 'react'
-import { Col, Row } from 'react-bootstrap'
-import { useTranslation } from 'react-i18next'
-import Tooltip from '../../../shared/components/tooltip'
-import { useGroupMembersContext } from '../context/group-members-context'
-import GroupMemberRow from './group-member-row'
-
-export default function GroupMembersList() {
-  const { t } = useTranslation()
-  const {
-    selectedUsers,
-    users,
-    selectUser,
-    unselectUser,
-    selectAllUsers,
-    unselectAllUsers,
-  } = useGroupMembersContext()
-
-  const handleSelectAllClick = useCallback(
-    (e: React.ChangeEvent<HTMLInputElement>) => {
-      if (e.target.checked) {
-        selectAllUsers()
-      } else {
-        unselectAllUsers()
-      }
-    },
-    [selectAllUsers, unselectAllUsers]
-  )
-
-  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>
-  )
-}

+ 2 - 8
services/web/frontend/js/features/group-management/components/group-members.tsx

@@ -6,8 +6,7 @@ import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n'
 import getMeta from '../../../utils/meta'
 import { useGroupMembersContext } from '../context/group-members-context'
 import ErrorAlert from './error-alert'
-import ManagedUsersList from './managed-users/managed-users-list'
-import GroupMembersList from './group-members-list'
+import MembersList from './members-table/members-list'
 
 export default function GroupMembers() {
   const { isReady } = useWaitForI18n()
@@ -28,7 +27,6 @@ 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 handleEmailsChange = useCallback(
     e => {
@@ -91,11 +89,7 @@ export default function GroupMembers() {
             </div>
             <div className="row-spaced-small">
               <ErrorAlert error={removeMemberError} />
-              {managedUsersActive ? (
-                <ManagedUsersList groupId={groupId} />
-              ) : (
-                <GroupMembersList />
-              )}
+              <MembersList groupId={groupId} />
             </div>
             <hr />
             {users.length < groupSize && (

+ 3 - 7
services/web/frontend/js/features/group-management/components/managers-table.tsx

@@ -1,17 +1,13 @@
 import { useCallback, useState } from 'react'
 import { Button, Col, Form, FormControl, Row } from 'react-bootstrap'
 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 { parseEmails } from '../utils/emails'
 import ErrorAlert, { APIError } from './error-alert'
-import GroupMemberRow from './group-member-row'
+import UserRow from './user-row'
 import useUserSelection from '../hooks/use-user-selection'
 import { User } from '../../../../../types/group-management/user'
 import { debugConsole } from '@/utils/debugging'
@@ -216,7 +212,7 @@ export function ManagersTable({
                   </li>
                 )}
                 {users.map(user => (
-                  <GroupMemberRow
+                  <UserRow
                     key={user.email}
                     user={user}
                     selectUser={selectUser}

+ 4 - 7
services/web/frontend/js/features/group-management/components/managed-users/managed-user-dropdown-button.tsx → services/web/frontend/js/features/group-management/components/members-table/dropdown-button.tsx

@@ -8,12 +8,9 @@ import {
 import { useTranslation } from 'react-i18next'
 import { Dropdown, MenuItem } from 'react-bootstrap'
 import { User } from '../../../../../../types/group-management/user'
-import useAsync from '../../../../shared/hooks/use-async'
-import {
-  type FetchError,
-  postJSON,
-} from '../../../../infrastructure/fetch-json'
-import Icon from '../../../../shared/components/icon'
+import useAsync from '@/shared/hooks/use-async'
+import { type FetchError, postJSON } from '@/infrastructure/fetch-json'
+import Icon from '@/shared/components/icon'
 import { ManagedUserAlert } from '../../utils/types'
 import { useGroupMembersContext } from '../../context/group-members-context'
 import getMeta from '@/utils/meta'
@@ -28,7 +25,7 @@ type ManagedUserDropdownButtonProps = {
   setManagedUserAlert: Dispatch<SetStateAction<ManagedUserAlert>>
 }
 
-export default function ManagedUserDropdownButton({
+export default function DropdownButton({
   user,
   openOffboardingModalForUser,
   groupId,

+ 1 - 1
services/web/frontend/js/features/group-management/components/managed-users/managed-users-list-alert.tsx → services/web/frontend/js/features/group-management/components/members-table/list-alert.tsx

@@ -9,7 +9,7 @@ type ManagedUsersListAlertProps = {
   onDismiss: () => void
 }
 
-export default function ManagedUsersListAlert({
+export default function ListAlert({
   variant,
   invitedUserEmail,
   onDismiss,

+ 1 - 1
services/web/frontend/js/features/group-management/components/managed-users/managed-user-status.tsx → services/web/frontend/js/features/group-management/components/members-table/managed-user-status.tsx

@@ -1,6 +1,6 @@
 import { useTranslation } from 'react-i18next'
 import { User } from '../../../../../../types/group-management/user'
-import MaterialIcon from '../../../../shared/components/material-icon'
+import MaterialIcon from '@/shared/components/material-icon'
 
 type ManagedUserStatusProps = {
   user: User

+ 14 - 11
services/web/frontend/js/features/group-management/components/managed-users/managed-user-row.tsx → services/web/frontend/js/features/group-management/components/members-table/member-row.tsx

@@ -7,8 +7,8 @@ import Tooltip from '../../../../shared/components/tooltip'
 import type { ManagedUserAlert } from '../../utils/types'
 import ManagedUserStatus from './managed-user-status'
 import SSOStatus from './sso-status'
-import ManagedUserDropdownButton from './managed-user-dropdown-button'
-import ManagedUsersSelectUserCheckbox from './managed-users-select-user-checkbox'
+import DropdownButton from './dropdown-button'
+import SelectUserCheckbox from './select-user-checkbox'
 import getMeta from '@/utils/meta'
 
 type ManagedUserRowProps = {
@@ -18,13 +18,14 @@ type ManagedUserRowProps = {
   setManagedUserAlert: Dispatch<SetStateAction<ManagedUserAlert>>
 }
 
-export default function ManagedUserRow({
+export default function MemberRow({
   user,
   openOffboardingModalForUser,
   setManagedUserAlert,
   groupId,
 }: ManagedUserRowProps) {
   const { t } = useTranslation()
+  const managedUsersActive: any = getMeta('ol-managedUsersActive')
   const groupSSOActive = getMeta('ol-groupSSOActive')
 
   return (
@@ -32,8 +33,8 @@ export default function ManagedUserRow({
       key={`user-${user.email}`}
       className={`managed-user-row ${user.invite ? 'text-muted' : ''}`}
     >
-      <ManagedUsersSelectUserCheckbox user={user} />
-      <td className={groupSSOActive ? 'cell-email-with-sso-col' : 'cell-email'}>
+      <SelectUserCheckbox user={user} />
+      <td className="cell-email">
         <span>
           {user.email}
           {user.invite ? (
@@ -83,13 +84,15 @@ export default function ManagedUserRow({
           </div>
         </td>
       )}
-      <td className="cell-managed">
-        <div className="managed-user-security">
-          <ManagedUserStatus user={user} />
-        </div>
-      </td>
+      {managedUsersActive && (
+        <td className="cell-managed">
+          <div className="managed-user-security">
+            <ManagedUserStatus user={user} />
+          </div>
+        </td>
+      )}
       <td className="cell-dropdown">
-        <ManagedUserDropdownButton
+        <DropdownButton
           user={user}
           openOffboardingModalForUser={openOffboardingModalForUser}
           setManagedUserAlert={setManagedUserAlert}

+ 28 - 20
services/web/frontend/js/features/group-management/components/managed-users/managed-users-list.tsx → services/web/frontend/js/features/group-management/components/members-table/members-list.tsx

@@ -2,20 +2,21 @@ import { useState } from 'react'
 import { Col, Row } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
 import { User } from '../../../../../../types/group-management/user'
-import Tooltip from '../../../../shared/components/tooltip'
+import Tooltip from '@/shared/components/tooltip'
 import { useGroupMembersContext } from '../../context/group-members-context'
 import type { ManagedUserAlert } from '../../utils/types'
-import ManagedUserRow from './managed-user-row'
+import MemberRow from './member-row'
 import OffboardManagedUserModal from './offboard-managed-user-modal'
-import ManagedUsersListAlert from './managed-users-list-alert'
-import ManagedUsersSelectAllCheckbox from './managed-users-select-all-checkbox'
+import ListAlert from './list-alert'
+import SelectAllCheckbox from './select-all-checkbox'
+import classNames from 'classnames'
 import getMeta from '@/utils/meta'
 
 type ManagedUsersListProps = {
   groupId: string
 }
 
-export default function ManagedUsersList({ groupId }: ManagedUsersListProps) {
+export default function MembersList({ groupId }: ManagedUsersListProps) {
   const { t } = useTranslation()
   const [userToOffboard, setUserToOffboard] = useState<User | undefined>(
     undefined
@@ -23,32 +24,37 @@ export default function ManagedUsersList({ groupId }: ManagedUsersListProps) {
   const [managedUserAlert, setManagedUserAlert] =
     useState<ManagedUserAlert>(undefined)
   const { users } = useGroupMembersContext()
+  const managedUsersActive: any = getMeta('ol-managedUsersActive')
   const groupSSOActive = getMeta('ol-groupSSOActive')
 
   return (
     <div>
-      {managedUserAlert && (
-        <ManagedUsersListAlert
+      {managedUsersActive && managedUserAlert && (
+        <ListAlert
           variant={managedUserAlert.variant}
           invitedUserEmail={managedUserAlert.email}
           onDismiss={() => setManagedUserAlert(undefined)}
         />
       )}
-      <ul className="list-unstyled structured-list managed-users-list">
+      <ul
+        className={classNames(
+          'list-unstyled',
+          'structured-list',
+          'managed-users-list',
+          {
+            'managed-users-active': managedUsersActive,
+            'group-sso-active': groupSSOActive,
+          }
+        )}
+      >
         <li className="container-fluid">
           <Row id="managed-users-list-headers">
             <Col xs={12}>
               <table className="managed-users-table">
                 <thead>
                   <tr>
-                    <ManagedUsersSelectAllCheckbox />
-                    <td
-                      className={
-                        groupSSOActive
-                          ? 'cell-email-with-sso-col'
-                          : 'cell-email'
-                      }
-                    >
+                    <SelectAllCheckbox />
+                    <td className="cell-email">
                       <span className="header">{t('email')}</span>
                     </td>
                     <td className="cell-name">
@@ -73,9 +79,11 @@ export default function ManagedUsersList({ groupId }: ManagedUsersListProps) {
                         <span className="header">{t('security')}</span>
                       </td>
                     )}
-                    <td className="cell-managed">
-                      <span className="header">{t('managed')}</span>
-                    </td>
+                    {managedUsersActive && (
+                      <td className="cell-managed">
+                        <span className="header">{t('managed')}</span>
+                      </td>
+                    )}
                     <td />
                   </tr>
                 </thead>
@@ -88,7 +96,7 @@ export default function ManagedUsersList({ groupId }: ManagedUsersListProps) {
                     </tr>
                   )}
                   {users.map((user: any) => (
-                    <ManagedUserRow
+                    <MemberRow
                       key={user.email}
                       user={user}
                       openOffboardingModalForUser={setUserToOffboard}

+ 5 - 5
services/web/frontend/js/features/group-management/components/managed-users/offboard-managed-user-modal.tsx → services/web/frontend/js/features/group-management/components/members-table/offboard-managed-user-modal.tsx

@@ -8,13 +8,13 @@ import {
   FormGroup,
   Modal,
 } from 'react-bootstrap'
-import AccessibleModal from '../../../../shared/components/accessible-modal'
-import Icon from '../../../../shared/components/icon'
+import AccessibleModal from '@/shared/components/accessible-modal'
+import Icon from '@/shared/components/icon'
 import { useState } from 'react'
-import useAsync from '../../../../shared/hooks/use-async'
+import useAsync from '@/shared/hooks/use-async'
 import { useTranslation } from 'react-i18next'
-import { useLocation } from '../../../../shared/hooks/use-location'
-import { FetchError, postJSON } from '../../../../infrastructure/fetch-json'
+import { useLocation } from '@/shared/hooks/use-location'
+import { FetchError, postJSON } from '@/infrastructure/fetch-json'
 import { debugConsole } from '@/utils/debugging'
 
 type OffboardManagedUserModalProps = {

+ 3 - 9
services/web/frontend/js/features/group-management/components/managed-users/managed-users-select-all-checkbox.tsx → services/web/frontend/js/features/group-management/components/members-table/select-all-checkbox.tsx

@@ -1,11 +1,9 @@
-import { useCallback } from 'react'
+import React, { useCallback } from 'react'
 import { useTranslation } from 'react-i18next'
 import { useGroupMembersContext } from '../../context/group-members-context'
-import getMeta from '@/utils/meta'
 
-export default function ManagedUsersSelectAllCheckbox() {
+export default function SelectAllCheckbox() {
   const { t } = useTranslation()
-  const groupSSOActive = getMeta('ol-groupSSOActive')
 
   const { selectedUsers, users, selectAllNonManagedUsers, unselectAllUsers } =
     useGroupMembersContext()
@@ -30,11 +28,7 @@ export default function ManagedUsersSelectAllCheckbox() {
   }
 
   return (
-    <td
-      className={
-        groupSSOActive ? 'cell-checkbox-with-sso-col ' : 'cell-checkbox'
-      }
-    >
+    <td className="cell-checkbox">
       <label htmlFor="select-all" className="sr-only">
         {t('select_all')}
       </label>

+ 2 - 8
services/web/frontend/js/features/group-management/components/managed-users/managed-users-select-user-checkbox.tsx → services/web/frontend/js/features/group-management/components/members-table/select-user-checkbox.tsx

@@ -2,17 +2,15 @@ import { useTranslation } from 'react-i18next'
 import type { User } from '../../../../../../types/group-management/user'
 import { useGroupMembersContext } from '../../context/group-members-context'
 import { useCallback } from 'react'
-import getMeta from '@/utils/meta'
 
 type ManagedUsersSelectUserCheckboxProps = {
   user: User
 }
 
-export default function ManagedUsersSelectUserCheckbox({
+export default function SelectUserCheckbox({
   user,
 }: ManagedUsersSelectUserCheckboxProps) {
   const { t } = useTranslation()
-  const groupSSOActive = getMeta('ol-groupSSOActive')
   const { users, selectedUsers, selectUser, unselectUser } =
     useGroupMembersContext()
 
@@ -40,11 +38,7 @@ export default function ManagedUsersSelectUserCheckbox({
   const selected = selectedUsers.includes(user)
 
   return (
-    <td
-      className={
-        groupSSOActive ? 'cell-checkbox-with-sso-col ' : 'cell-checkbox'
-      }
-    >
+    <td className="cell-checkbox">
       {/* the next check will hide the `checkbox` but still show the `td` */}
       {user.enrollment?.managedBy ? null : (
         <>

+ 2 - 2
services/web/frontend/js/features/group-management/components/managed-users/sso-status.tsx → services/web/frontend/js/features/group-management/components/members-table/sso-status.tsx

@@ -19,13 +19,13 @@ export default function SSOStatus({ user }: SSOStatusProps) {
   )
   const acceptedSSO = (
     <span className="security-state-managed">
-      <MaterialIcon type="check" accessibilityLabel={t('sso_linked')} />
+      <MaterialIcon type="check" accessibilityLabel={t('sso_active')} />
       &nbsp; {t('sso')}
     </span>
   )
   const notAcceptedSSO = (
     <span className="security-state-not-managed">
-      <MaterialIcon type="close" accessibilityLabel={t('sso_unlinked')} />
+      <MaterialIcon type="close" accessibilityLabel={t('sso_not_active')} />
       &nbsp; {t('sso')}
     </span>
   )

+ 1 - 1
services/web/frontend/js/features/group-management/components/group-member-row.tsx → services/web/frontend/js/features/group-management/components/user-row.tsx

@@ -11,7 +11,7 @@ type GroupMemberRowProps = {
   selected: boolean
 }
 
-export default function GroupMemberRow({
+export default function UserRow({
   user,
   selectUser,
   unselectUser,

+ 3 - 7
services/web/frontend/js/features/group-management/context/group-members-context.tsx

@@ -7,13 +7,9 @@ import {
   useState,
 } from 'react'
 import { User } from '../../../../../types/group-management/user'
-import {
-  deleteJSON,
-  FetchError,
-  postJSON,
-} from '../../../infrastructure/fetch-json'
-import { mapSeries } from '../../../infrastructure/promise'
-import getMeta from '../../../utils/meta'
+import { deleteJSON, FetchError, postJSON } from '@/infrastructure/fetch-json'
+import { mapSeries } from '@/infrastructure/promise'
+import getMeta from '@/utils/meta'
 import { APIError } from '../components/error-alert'
 import useUserSelection from '../hooks/use-user-selection'
 import { parseEmails } from '../utils/emails'

+ 36 - 64
services/web/frontend/stylesheets/components/group-members.less

@@ -103,75 +103,39 @@
     }
   }
 
-  .cell-checkbox {
-    width: 5%;
-  }
-  .cell-checkbox-with-sso-col {
-    width: 2.5%;
-  }
-
-  .cell-email {
-    width: 45%;
-  }
-  .cell-email-with-sso-col {
-    width: 37%;
-  }
-
-  .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: 10%;
-    overflow-x: hidden;
-    text-overflow: ellipsis;
-    white-space: nowrap;
-  }
-
-  .cell-managed {
-    width: 15%;
-    overflow-x: hidden;
-    text-overflow: ellipsis;
-    white-space: nowrap;
-  }
-
-  .cell-dropdown {
-    width: 5%;
-    min-width: 25px;
-  }
-
   @media (min-width: @screen-xs) {
     .cell-checkbox {
       width: 5%;
-    }
-
-    .cell-checkbox-with-sso-col {
-      width: 2.5%;
+      .managed-users-active.group-sso-active & {
+        width: 3%;
+      }
     }
 
     .cell-email {
-      width: 34%;
-    }
-    .cell-email-with-sso-col {
-      width: 29%;
+      width: 50%;
+      .managed-users-active & {
+        width: 35%;
+      }
+      .group-sso-active & {
+        width: 37%;
+      }
+      .managed-users-active.group-sso-active & {
+        width: 29%;
+      }
     }
 
     .cell-name {
       width: 20%;
+      .managed-users-active.group-sso-active & {
+        width: 18%;
+      }
     }
 
     .cell-last-active {
       width: 20%;
+      .managed-users-active.group-sso-active & {
+        width: 16%;
+      }
     }
 
     .cell-security {
@@ -191,20 +155,28 @@
   @media (min-width: @screen-lg) {
     .cell-checkbox {
       width: 5%;
+      .managed-users-active.group-sso-active & {
+        width: 3%;
+      }
     }
-    .cell-checkbox-with-sso-col {
-      width: 2.5%;
-    }
-
     .cell-email {
-      width: 43%;
-    }
-    .cell-email-with-sso-col {
-      width: 37%;
+      width: 55%;
+      .managed-users-active & {
+        width: 42%;
+      }
+      .group-sso-active & {
+        width: 45%;
+      }
+      .managed-users-active.group-sso-active & {
+        width: 36%;
+      }
     }
 
     .cell-name {
       width: 20%;
+      .managed-users-active.group-sso-active & {
+        width: 18%;
+      }
     }
 
     .cell-last-active {
@@ -216,7 +188,7 @@
     }
 
     .cell-managed {
-      width: 12%;
+      width: 13%;
     }
 
     .cell-dropdown {

+ 2 - 2
services/web/locales/en.json

@@ -1644,6 +1644,7 @@
   "spread_the_word_and_fill_bar": "Spread the word and fill this bar up",
   "sso": "SSO",
   "sso_account_already_linked": "Account already linked to another __appName__ user",
+  "sso_active": "SSO active",
   "sso_config_prop_help_certificate": "Base64 encoded certificate without whitespace",
   "sso_config_prop_help_first_name": "Property in SAML assertion to use for first name",
   "sso_config_prop_help_last_name": "Property in SAML assertion to use for last name",
@@ -1664,10 +1665,9 @@
   "sso_is_enabled_explanation_2": "If there are any problems with the configuration, only you (as the group administrator) will be able to disable SSO.",
   "sso_link_error": "Error linking account",
   "sso_link_invite_has_been_sent_to_email": "An SSO invite reminder has been sent to <0>__email__</0>",
-  "sso_linked": "SSO linked",
   "sso_logs": "SSO Logs",
+  "sso_not_active": "SSO not active",
   "sso_not_linked": "You have not linked your account to __provider__. Please log in to your account another way and link your __provider__ account via your account settings.",
-  "sso_unlinked": "SSO unlinked",
   "sso_user_denied_access": "Cannot log in because __appName__ was not granted access to your __provider__ account. Please try again.",
   "standard": "Standard",
   "start_a_free_trial": "Start a free trial",

+ 1 - 1
services/web/test/frontend/features/group-management/components/group-managers.spec.tsx

@@ -1,4 +1,4 @@
-import GroupManagers from '../../../../../frontend/js/features/group-management/components/group-managers'
+import GroupManagers from '@/features/group-management/components/group-managers'
 
 const JOHN_DOE = {
   _id: 'abc123def456',

+ 386 - 105
services/web/test/frontend/features/group-management/components/group-members.spec.tsx

@@ -1,22 +1,6 @@
-import GroupMembers from '../../../../../frontend/js/features/group-management/components/group-members'
-import { GroupMembersProvider } from '../../../../../frontend/js/features/group-management/context/group-members-context'
-
-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,
-}
+import GroupMembers from '@/features/group-management/components/group-members'
+import { GroupMembersProvider } from '@/features/group-management/context/group-members-context'
+
 const GROUP_ID = '777fff777fff'
 const PATHS = {
   addMember: `/manage/groups/${GROUP_ID}/invites`,
@@ -25,139 +9,436 @@ const PATHS = {
   exportMembers: `/manage/groups/${GROUP_ID}/members/export`,
 }
 
-describe('group members, without managed users', function () {
-  beforeEach(function () {
-    cy.window().then(win => {
-      win.metaAttributesCache.set('ol-groupId', GROUP_ID)
-      win.metaAttributesCache.set('ol-groupName', 'My Awesome Team')
-      win.metaAttributesCache.set('ol-groupSize', 10)
-      win.metaAttributesCache.set('ol-users', [JOHN_DOE, BOBBY_LAPOINTE])
-    })
-
+describe('GroupMembers', function () {
+  function mountGroupMembersProvider() {
     cy.mount(
       <GroupMembersProvider>
         <GroupMembers />
       </GroupMembersProvider>
     )
-  })
+  }
 
-  it('renders the group members page', function () {
-    cy.get('h1').contains('My Awesome Team')
-    cy.get('small').contains('You have added 2 of 10 available members')
+  describe('with Managed Users and Group SSO disabled', function () {
+    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,
+    }
 
-    cy.get('ul').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="Invite not yet accepted"]`)
+    beforeEach(function () {
+      cy.window().then(win => {
+        win.metaAttributesCache.set('ol-groupId', GROUP_ID)
+        win.metaAttributesCache.set('ol-groupName', 'My Awesome Team')
+        win.metaAttributesCache.set('ol-groupSize', 10)
+        win.metaAttributesCache.set('ol-users', [JOHN_DOE, BOBBY_LAPOINTE])
       })
 
-      cy.get('li:nth-child(3)').within(() => {
-        cy.contains('bobby.lapointe@test.com')
-        cy.contains('Bobby Lapointe')
-        cy.contains('2nd Jan 2023')
-        cy.get(`[aria-label="Accepted invite"]`)
+      cy.mount(
+        <GroupMembersProvider>
+          <GroupMembers />
+        </GroupMembersProvider>
+      )
+    })
+
+    it('renders the group members page', function () {
+      cy.get('h1').contains('My Awesome Team')
+      cy.get('small').contains('You have added 2 of 10 available members')
+
+      cy.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('.badge-new-comment').contains('Pending invite')
+        })
+
+        cy.get('tr:nth-child(2)').within(() => {
+          cy.contains('bobby.lapointe@test.com')
+          cy.contains('Bobby Lapointe')
+          cy.contains('2nd Jan 2023')
+          cy.get('.badge-new-comment').should('not.exist')
+        })
       })
     })
-  })
 
-  it('sends an invite', function () {
-    cy.intercept('POST', PATHS.addMember, {
-      statusCode: 201,
-      body: {
-        user: {
-          email: 'someone.else@test.com',
-          invite: true,
+    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 table > tbody').within(() => {
+        cy.get('tr:nth-child(3)').within(() => {
+          cy.contains('someone.else@test.com')
+          cy.contains('N/A')
+          cy.get('.badge-new-comment').contains('Pending invite')
+        })
+      })
+    })
+
+    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')
     })
 
-    cy.get('.form-control').type('someone.else@test.com')
-    cy.get('button').click()
+    it('checks the select all checkbox', function () {
+      cy.get('ul.managed-users-list table > tbody').within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.get('.select-item').should('not.be.checked')
+        })
+        cy.get('tr:nth-child(2)').within(() => {
+          cy.get('.select-item').should('not.be.checked')
+        })
+      })
 
-    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.get('.select-all').click()
+
+      cy.get('ul.managed-users-list table > tbody').within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.get('.select-item').should('be.checked')
+        })
+        cy.get('tr:nth-child(2)').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 table > tbody').within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.get('.select-item').check()
+        })
+      })
+
+      cy.get('button').contains('Remove from group').click()
+
+      cy.get('small').contains('You have added 1 of 10 available members')
+      cy.get('ul.managed-users-list table > tbody').within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.contains('bobby.lapointe@test.com')
+          cy.contains('Bobby Lapointe')
+          cy.contains('2nd Jan 2023')
+          cy.contains('Pending invite').should('not.exist')
+        })
+      })
+    })
+
+    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 table > tbody').within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.get('.select-item').check()
+        })
+      })
+      cy.get('button').contains('Remove from group').click()
+
+      cy.get('.alert').contains('Sorry, something went wrong')
+    })
   })
 
-  it('tries to send an invite and displays the error', function () {
-    cy.intercept('POST', PATHS.addMember, {
-      statusCode: 500,
-      body: {
-        error: {
-          message: 'User already added',
+  describe('with Managed Users enabled', function () {
+    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'),
+        sso: {
+          providerId: '123',
+          externalId: '123',
         },
       },
+    }
+
+    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)
+        win.metaAttributesCache.set('ol-managedUsersActive', true)
+      })
+      mountGroupMembersProvider()
     })
 
-    cy.get('.form-control').type('someone.else@test.com')
-    cy.get('button').click()
-    cy.get('.alert').contains('Error: User already added')
-  })
+    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 table > tbody').within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.contains('john.doe@test.com')
+          cy.contains('John Doe')
+          cy.contains('15th Jan 2023')
+          cy.get('.sr-only').contains('Pending invite')
+          cy.get('.badge-new-comment').contains('Pending invite')
+          cy.get(`.security-state-invite-pending`).should('exist')
+
+          cy.get('.badge-new-comment').contains('Pending invite')
+          cy.get(`.security-state-invite-pending`).should('exist')
+        })
+
+        cy.get('tr:nth-child(2)').within(() => {
+          cy.contains('bobby.lapointe@test.com')
+          cy.contains('Bobby Lapointe')
+          cy.contains('2nd Jan 2023')
+          cy.get('.badge-new-comment').should('not.exist')
+          cy.get('.sr-only').contains('Not managed')
+        })
 
-  it('checks the select all checkbox', function () {
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.get('.select-item').should('not.be.checked')
+        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('.sr-only').contains('Managed')
+        })
       })
-      cy.get('li:nth-child(3)').within(() => {
-        cy.get('.select-item').should('not.be.checked')
+    })
+
+    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 table > tbody').within(() => {
+        cy.get('tr:nth-child(4)').within(() => {
+          cy.contains('someone.else@test.com')
+          cy.contains('N/A')
+          cy.get('.sr-only').contains('Pending invite')
+          cy.get('.badge-new-comment').contains('Pending invite')
+          cy.get(`.security-state-invite-pending`).should('exist')
+        })
       })
     })
 
-    cy.get('.select-all').click()
+    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')
+    })
 
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.get('.select-item').should('be.checked')
+    it('checks the select all checkbox', function () {
+      cy.get('ul.managed-users-list table > tbody').within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.get('.select-item').should('not.be.checked')
+        })
+        cy.get('tr:nth-child(2)').within(() => {
+          cy.get('.select-item').should('not.be.checked')
+        })
       })
-      cy.get('li:nth-child(3)').within(() => {
-        cy.get('.select-item').should('be.checked')
+
+      cy.get('.select-all').click()
+
+      cy.get('ul.managed-users-list table > tbody').within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.get('.select-item').should('be.checked')
+        })
+        cy.get('tr:nth-child(2)').within(() => {
+          cy.get('.select-item').should('be.checked')
+        })
       })
+
+      cy.get('button').contains('Remove from group').click()
     })
-  })
 
-  it('remove a member', function () {
-    cy.intercept('DELETE', `${PATHS.removeMember}/abc123def456`, {
-      statusCode: 200,
+    it('remove a member', function () {
+      cy.intercept('DELETE', `${PATHS.removeMember}/abc123def456`, {
+        statusCode: 200,
+      })
+
+      cy.get('ul.managed-users-list table > tbody').within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.get('.select-item').check()
+        })
+      })
+
+      cy.get('button').contains('Remove from group').click()
+
+      cy.get('small').contains('You have added 2 of 10 available members')
+      cy.get('ul.managed-users-list table > tbody').within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.contains('bobby.lapointe@test.com')
+          cy.contains('Bobby Lapointe')
+          cy.contains('2nd Jan 2023')
+        })
+      })
     })
 
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.get('.select-item').check()
+    it('cannot remove a managed member', function () {
+      cy.intercept('DELETE', `${PATHS.removeMember}/abc123def456`, {
+        statusCode: 200,
+      })
+
+      cy.get('ul.managed-users-list table > tbody').within(() => {
+        // no checkbox should be shown for 'Claire Jennings', a managed user
+        cy.get('tr:nth-child(3)').within(() => {
+          cy.get('.select-item').should('not.exist')
+        })
       })
     })
 
-    cy.get('button').contains('Remove from group').click()
+    it('tries to remove a user and displays the error', function () {
+      cy.intercept('DELETE', `${PATHS.removeMember}/abc123def456`, {
+        statusCode: 500,
+      })
 
-    cy.get('small').contains('You have added 1 of 10 available members')
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.contains('bobby.lapointe@test.com')
-        cy.contains('Bobby Lapointe')
-        cy.contains('2nd Jan 2023')
-        cy.get(`[aria-label="Accepted invite"]`)
+      cy.get('ul.managed-users-list table > tbody').within(() => {
+        cy.get('tr:nth-child(1)').within(() => {
+          cy.get('.select-item').check()
+        })
       })
+      cy.get('.page-header').within(() => {
+        cy.get('button').contains('Remove from group').click()
+      })
+
+      cy.get('.alert').contains('Sorry, something went wrong')
     })
   })
 
-  it('tries to remove a user and displays the error', function () {
-    cy.intercept('DELETE', `${PATHS.removeMember}/abc123def456`, {
-      statusCode: 500,
-    })
+  describe('with Group SSO enabled', function () {
+    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'),
+        sso: {
+          providerId: '123',
+          externalId: '123',
+        },
+      },
+    }
 
-    cy.get('ul').within(() => {
-      cy.get('li:nth-child(2)').within(() => {
-        cy.get('.select-item').check()
+    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)
+        win.metaAttributesCache.set('ol-managedUsersActive', false)
+        win.metaAttributesCache.set('ol-groupSSOActive', true)
       })
+
+      mountGroupMembersProvider()
     })
-    cy.get('button').contains('Remove from group').click()
 
-    cy.get('.alert').contains('Sorry, something went wrong')
+    it('should display the Security column', function () {
+      cy.get('ul.managed-users-list table > tbody').within(() => {
+        cy.get('tr:nth-child(2)').within(() => {
+          cy.contains('bobby.lapointe@test.com')
+          cy.get('.sr-only').contains('SSO not active')
+        })
+
+        cy.get('tr:nth-child(3)').within(() => {
+          cy.contains('claire.jennings@test.com')
+          cy.get('.sr-only').contains('SSO active')
+        })
+      })
+    })
   })
 })

+ 1 - 1
services/web/test/frontend/features/group-management/components/institution-managers.spec.tsx

@@ -1,4 +1,4 @@
-import InstitutionManagers from '../../../../../frontend/js/features/group-management/components/institution-managers'
+import InstitutionManagers from '@/features/group-management/components/institution-managers'
 
 const JOHN_DOE = {
   _id: 'abc123def456',

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

@@ -1,5 +1,5 @@
-import GroupMembers from '../../../../../../frontend/js/features/group-management/components/group-members'
-import { GroupMembersProvider } from '../../../../../../frontend/js/features/group-management/context/group-members-context'
+import GroupMembers from '@/features/group-management/components/group-members'
+import { GroupMembersProvider } from '@/features/group-management/context/group-members-context'
 
 const GROUP_ID = '777fff777fff'
 const JOHN_DOE = {
@@ -241,16 +241,17 @@ describe('Group members when group SSO is enabled', function () {
     cy.get('ul.managed-users-list table > tbody').within(() => {
       cy.get('tr:nth-child(2)').within(() => {
         cy.contains('bobby.lapointe@test.com')
-        cy.get('.sr-only').contains('SSO unlinked').should('not.exist')
+        cy.get('.sr-only').contains('SSO not active').should('not.exist')
       })
 
       cy.get('tr:nth-child(3)').within(() => {
         cy.contains('claire.jennings@test.com')
-        cy.get('.sr-only').contains('SSO linked').should('not.exist')
+        cy.get('.sr-only').contains('SSO active').should('not.exist')
       })
     })
   })
-  it('should display SSO Column when group sso is not enabled', function () {
+
+  it('should display SSO Column when Group SSO is enabled', function () {
     cy.window().then(win => {
       win.metaAttributesCache.set('ol-groupSSOActive', true)
     })
@@ -258,12 +259,12 @@ describe('Group members when group SSO is enabled', function () {
     cy.get('ul.managed-users-list table > tbody').within(() => {
       cy.get('tr:nth-child(2)').within(() => {
         cy.contains('bobby.lapointe@test.com')
-        cy.get('.sr-only').contains('SSO unlinked')
+        cy.get('.sr-only').contains('SSO not active')
       })
 
       cy.get('tr:nth-child(3)').within(() => {
         cy.contains('claire.jennings@test.com')
-        cy.get('.sr-only').contains('SSO linked')
+        cy.get('.sr-only').contains('SSO active')
       })
     })
   })

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

@@ -1,167 +0,0 @@
-import sinon from 'sinon'
-import ManagedUserRow from '../../../../../../frontend/js/features/group-management/components/managed-users/managed-user-row'
-import { GroupMembersProvider } from '../../../../../../frontend/js/features/group-management/context/group-members-context'
-import { User } from '../../../../../../types/group-management/user'
-
-describe('ManagedUserRow', function () {
-  const subscriptionId = '123abc'
-
-  describe('with an ordinary user', function () {
-    let user: User
-
-    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,
-      }
-      cy.window().then(win => {
-        win.metaAttributesCache.set('ol-users', [user])
-      })
-
-      cy.mount(
-        <GroupMembersProvider>
-          <ManagedUserRow
-            user={user}
-            openOffboardingModalForUser={sinon.stub()}
-            groupId={subscriptionId}
-            setManagedUserAlert={sinon.stub()}
-          />
-        </GroupMembersProvider>
-      )
-    })
-
-    it('renders the row', function () {
-      cy.get('tr').should('exist')
-      // Checkbox
-      cy.get('.select-item').should('not.be.checked')
-      // Email
-      cy.get('tr').contains(user.email)
-      // Name
-      cy.get('tr').contains(user.first_name)
-      cy.get('tr').contains(user.last_name)
-      // Last active date
-      cy.get('tr').contains('21st Nov 2070')
-      // Managed status
-      cy.get('tr').contains('Managed')
-      // Dropdown button
-      cy.get('#managed-user-dropdown-some\\.user\\@example\\.com').should(
-        'exist'
-      )
-    })
-  })
-
-  describe('with a pending invite', function () {
-    let user: User
-
-    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,
-      }
-      cy.window().then(win => {
-        win.metaAttributesCache.set('ol-users', [user])
-      })
-
-      cy.mount(
-        <GroupMembersProvider>
-          <ManagedUserRow
-            user={user}
-            openOffboardingModalForUser={sinon.stub()}
-            groupId={subscriptionId}
-            setManagedUserAlert={sinon.stub()}
-          />
-        </GroupMembersProvider>
-      )
-    })
-
-    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
-
-    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,
-      }
-      cy.window().then(win => {
-        win.metaAttributesCache.set('ol-users', [user])
-      })
-
-      cy.mount(
-        <GroupMembersProvider>
-          <ManagedUserRow
-            user={user}
-            openOffboardingModalForUser={sinon.stub()}
-            groupId={subscriptionId}
-            setManagedUserAlert={sinon.stub()}
-          />
-        </GroupMembersProvider>
-      )
-    })
-
-    it('should render a "Group admin" symbol', function () {
-      cy.get('[aria-label="Group admin"].fa-user-circle-o').should('exist')
-    })
-  })
-
-  describe('selecting and unselecting user row', function () {
-    let user: User
-
-    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,
-      }
-      cy.window().then(win => {
-        win.metaAttributesCache.set('ol-users', [user])
-      })
-
-      cy.mount(
-        <GroupMembersProvider>
-          <ManagedUserRow
-            user={user}
-            openOffboardingModalForUser={sinon.stub()}
-            groupId={subscriptionId}
-            setManagedUserAlert={sinon.stub()}
-          />
-        </GroupMembersProvider>
-      )
-    })
-
-    it('should select and unselect the user', function () {
-      cy.get('.select-item').should('not.be.checked')
-      cy.get('.select-item').click()
-      cy.get('.select-item').should('be.checked')
-      cy.get('.select-item').click()
-      cy.get('.select-item').should('not.be.checked')
-    })
-  })
-})

+ 4 - 3
services/web/test/frontend/features/group-management/components/managed-users/managed-user-dropdown-button.spec.tsx → services/web/test/frontend/features/group-management/components/members-table/dropdown-button.spec.tsx

@@ -1,7 +1,7 @@
 import type { PropsWithChildren } from 'react'
 import sinon from 'sinon'
-import ManagedUserDropdownButton from '../../../../../../frontend/js/features/group-management/components/managed-users/managed-user-dropdown-button'
-import { GroupMembersProvider } from '../../../../../../frontend/js/features/group-management/context/group-members-context'
+import DropdownButton from '@/features/group-management/components/members-table/dropdown-button'
+import { GroupMembersProvider } from '@/features/group-management/context/group-members-context'
 import { User } from '../../../../../../types/group-management/user'
 
 function Wrapper({ children }: PropsWithChildren<Record<string, unknown>>) {
@@ -20,7 +20,7 @@ function Wrapper({ children }: PropsWithChildren<Record<string, unknown>>) {
 function mountDropDownComponent(user: User, subscriptionId: string) {
   cy.mount(
     <Wrapper>
-      <ManagedUserDropdownButton
+      <DropdownButton
         user={user}
         openOffboardingModalForUser={sinon.stub()}
         groupId={subscriptionId}
@@ -218,6 +218,7 @@ describe('ManagedUserDropdownButton', function () {
       cy.findByTestId('no-actions-available').should('exist')
     })
   })
+
   describe('sending SSO invite reminder', function () {
     const user = {
       _id: 'some-user',

+ 2 - 2
services/web/test/frontend/features/group-management/components/managed-users/managed-user-status.spec.tsx → services/web/test/frontend/features/group-management/components/members-table/managed-user-status.spec.tsx

@@ -1,7 +1,7 @@
-import ManagedUserStatus from '../../../../../../frontend/js/features/group-management/components/managed-users/managed-user-status'
+import ManagedUserStatus from '@/features/group-management/components/members-table/managed-user-status'
 import { User } from '../../../../../../types/group-management/user'
 
-describe('ManagedUserStatus', function () {
+describe('MemberStatus', function () {
   describe('with a pending invite', function () {
     const user: User = {
       _id: 'some-user',

+ 685 - 0
services/web/test/frontend/features/group-management/components/members-table/member-row.spec.tsx

@@ -0,0 +1,685 @@
+import sinon from 'sinon'
+import MemberRow from '@/features/group-management/components/members-table/member-row'
+import { GroupMembersProvider } from '@/features/group-management/context/group-members-context'
+import { User } from '../../../../../../types/group-management/user'
+
+describe('MemberRow', function () {
+  const subscriptionId = '123abc'
+
+  describe('default view', function () {
+    beforeEach(function () {
+      cy.window().then(win => {
+        win.metaAttributesCache = new Map()
+      })
+    })
+
+    describe('with an ordinary user', function () {
+      let user: User
+
+      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,
+        }
+        cy.window().then(win => {
+          win.metaAttributesCache.set('ol-users', [user])
+        })
+
+        cy.mount(
+          <GroupMembersProvider>
+            <MemberRow
+              user={user}
+              openOffboardingModalForUser={sinon.stub()}
+              groupId={subscriptionId}
+              setManagedUserAlert={sinon.stub()}
+            />
+          </GroupMembersProvider>
+        )
+      })
+
+      it('renders the row', function () {
+        cy.get('tr').should('exist')
+        // Checkbox
+        cy.get('.select-item').should('not.be.checked')
+        // Email
+        cy.get('tr').contains(user.email)
+        // Name
+        cy.get('tr').contains(user.first_name)
+        cy.get('tr').contains(user.last_name)
+        // Last active date
+        cy.get('tr').contains('21st Nov 2070')
+        // Dropdown button
+        cy.get('#managed-user-dropdown-some\\.user\\@example\\.com').should(
+          'exist'
+        )
+
+        cy.get('tr').contains('SSO').should('not.exist')
+        cy.get('tr').contains('Managed').should('not.exist')
+      })
+    })
+
+    describe('with a pending invite', function () {
+      let user: User
+
+      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,
+        }
+        cy.window().then(win => {
+          win.metaAttributesCache.set('ol-users', [user])
+        })
+
+        cy.mount(
+          <GroupMembersProvider>
+            <MemberRow
+              user={user}
+              openOffboardingModalForUser={sinon.stub()}
+              groupId={subscriptionId}
+              setManagedUserAlert={sinon.stub()}
+            />
+          </GroupMembersProvider>
+        )
+      })
+
+      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
+
+      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,
+        }
+        cy.window().then(win => {
+          win.metaAttributesCache.set('ol-users', [user])
+        })
+
+        cy.mount(
+          <GroupMembersProvider>
+            <MemberRow
+              user={user}
+              openOffboardingModalForUser={sinon.stub()}
+              groupId={subscriptionId}
+              setManagedUserAlert={sinon.stub()}
+            />
+          </GroupMembersProvider>
+        )
+      })
+
+      it('should render a "Group admin" symbol', function () {
+        cy.get('[aria-label="Group admin"].fa-user-circle-o').should('exist')
+      })
+    })
+
+    describe('selecting and unselecting user row', function () {
+      let user: User
+
+      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,
+        }
+        cy.window().then(win => {
+          win.metaAttributesCache.set('ol-users', [user])
+        })
+
+        cy.mount(
+          <GroupMembersProvider>
+            <MemberRow
+              user={user}
+              openOffboardingModalForUser={sinon.stub()}
+              groupId={subscriptionId}
+              setManagedUserAlert={sinon.stub()}
+            />
+          </GroupMembersProvider>
+        )
+      })
+
+      it('should select and unselect the user', function () {
+        cy.get('.select-item').should('not.be.checked')
+        cy.get('.select-item').click()
+        cy.get('.select-item').should('be.checked')
+        cy.get('.select-item').click()
+        cy.get('.select-item').should('not.be.checked')
+      })
+    })
+  })
+
+  describe('with Managed Users enabled', function () {
+    beforeEach(function () {
+      cy.window().then(win => {
+        win.metaAttributesCache = new Map()
+        win.metaAttributesCache.set('ol-managedUsersActive', true)
+      })
+    })
+
+    describe('with an ordinary user', function () {
+      let user: User
+
+      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,
+        }
+        cy.window().then(win => {
+          win.metaAttributesCache.set('ol-users', [user])
+        })
+
+        cy.mount(
+          <GroupMembersProvider>
+            <MemberRow
+              user={user}
+              openOffboardingModalForUser={sinon.stub()}
+              groupId={subscriptionId}
+              setManagedUserAlert={sinon.stub()}
+            />
+          </GroupMembersProvider>
+        )
+      })
+
+      it('renders the row', function () {
+        cy.get('tr').should('exist')
+        // Checkbox
+        cy.get('.select-item').should('not.be.checked')
+        // Email
+        cy.get('tr').contains(user.email)
+        // Name
+        cy.get('tr').contains(user.first_name)
+        cy.get('tr').contains(user.last_name)
+        // Last active date
+        cy.get('tr').contains('21st Nov 2070')
+        // Managed status
+        cy.get('tr').contains('Managed')
+        // Dropdown button
+        cy.get('#managed-user-dropdown-some\\.user\\@example\\.com').should(
+          'exist'
+        )
+      })
+    })
+
+    describe('with a pending invite', function () {
+      let user: User
+
+      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,
+        }
+        cy.window().then(win => {
+          win.metaAttributesCache.set('ol-users', [user])
+        })
+
+        cy.mount(
+          <GroupMembersProvider>
+            <MemberRow
+              user={user}
+              openOffboardingModalForUser={sinon.stub()}
+              groupId={subscriptionId}
+              setManagedUserAlert={sinon.stub()}
+            />
+          </GroupMembersProvider>
+        )
+      })
+
+      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
+
+      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,
+        }
+        cy.window().then(win => {
+          win.metaAttributesCache.set('ol-users', [user])
+        })
+
+        cy.mount(
+          <GroupMembersProvider>
+            <MemberRow
+              user={user}
+              openOffboardingModalForUser={sinon.stub()}
+              groupId={subscriptionId}
+              setManagedUserAlert={sinon.stub()}
+            />
+          </GroupMembersProvider>
+        )
+      })
+
+      it('should render a "Group admin" symbol', function () {
+        cy.get('[aria-label="Group admin"].fa-user-circle-o').should('exist')
+      })
+    })
+
+    describe('selecting and unselecting user row', function () {
+      let user: User
+
+      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,
+        }
+        cy.window().then(win => {
+          win.metaAttributesCache.set('ol-users', [user])
+        })
+
+        cy.mount(
+          <GroupMembersProvider>
+            <MemberRow
+              user={user}
+              openOffboardingModalForUser={sinon.stub()}
+              groupId={subscriptionId}
+              setManagedUserAlert={sinon.stub()}
+            />
+          </GroupMembersProvider>
+        )
+      })
+
+      it('should select and unselect the user', function () {
+        cy.get('.select-item').should('not.be.checked')
+        cy.get('.select-item').click()
+        cy.get('.select-item').should('be.checked')
+        cy.get('.select-item').click()
+        cy.get('.select-item').should('not.be.checked')
+      })
+    })
+  })
+
+  describe('with Group SSO enabled', function () {
+    beforeEach(function () {
+      cy.window().then(win => {
+        win.metaAttributesCache = new Map()
+        win.metaAttributesCache.set('ol-groupSSOActive', true)
+      })
+    })
+
+    describe('with an ordinary user', function () {
+      let user: User
+
+      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,
+        }
+        cy.window().then(win => {
+          win.metaAttributesCache.set('ol-users', [user])
+        })
+
+        cy.mount(
+          <GroupMembersProvider>
+            <MemberRow
+              user={user}
+              openOffboardingModalForUser={sinon.stub()}
+              groupId={subscriptionId}
+              setManagedUserAlert={sinon.stub()}
+            />
+          </GroupMembersProvider>
+        )
+      })
+
+      it('renders the row', function () {
+        cy.get('tr').should('exist')
+        // Checkbox
+        cy.get('.select-item').should('not.be.checked')
+        // Email
+        cy.get('tr').contains(user.email)
+        // Name
+        cy.get('tr').contains(user.first_name)
+        cy.get('tr').contains(user.last_name)
+        // Last active date
+        cy.get('tr').contains('21st Nov 2070')
+        // SSO status
+        cy.get('tr').contains('SSO')
+        // Dropdown button
+        cy.get('#managed-user-dropdown-some\\.user\\@example\\.com').should(
+          'exist'
+        )
+
+        cy.get('tr').contains('Managed').should('not.exist')
+      })
+    })
+
+    describe('with a pending invite', function () {
+      let user: User
+
+      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,
+        }
+        cy.window().then(win => {
+          win.metaAttributesCache.set('ol-users', [user])
+        })
+
+        cy.mount(
+          <GroupMembersProvider>
+            <MemberRow
+              user={user}
+              openOffboardingModalForUser={sinon.stub()}
+              groupId={subscriptionId}
+              setManagedUserAlert={sinon.stub()}
+            />
+          </GroupMembersProvider>
+        )
+      })
+
+      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
+
+      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,
+        }
+        cy.window().then(win => {
+          win.metaAttributesCache.set('ol-users', [user])
+        })
+
+        cy.mount(
+          <GroupMembersProvider>
+            <MemberRow
+              user={user}
+              openOffboardingModalForUser={sinon.stub()}
+              groupId={subscriptionId}
+              setManagedUserAlert={sinon.stub()}
+            />
+          </GroupMembersProvider>
+        )
+      })
+
+      it('should render a "Group admin" symbol', function () {
+        cy.get('[aria-label="Group admin"].fa-user-circle-o').should('exist')
+      })
+    })
+
+    describe('selecting and unselecting user row', function () {
+      let user: User
+
+      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,
+        }
+        cy.window().then(win => {
+          win.metaAttributesCache.set('ol-users', [user])
+        })
+
+        cy.mount(
+          <GroupMembersProvider>
+            <MemberRow
+              user={user}
+              openOffboardingModalForUser={sinon.stub()}
+              groupId={subscriptionId}
+              setManagedUserAlert={sinon.stub()}
+            />
+          </GroupMembersProvider>
+        )
+      })
+
+      it('should select and unselect the user', function () {
+        cy.get('.select-item').should('not.be.checked')
+        cy.get('.select-item').click()
+        cy.get('.select-item').should('be.checked')
+        cy.get('.select-item').click()
+        cy.get('.select-item').should('not.be.checked')
+      })
+    })
+  })
+
+  describe('with Managed Users and Group SSO enabled', function () {
+    beforeEach(function () {
+      cy.window().then(win => {
+        win.metaAttributesCache = new Map()
+        win.metaAttributesCache.set('ol-managedUsersActive', true)
+        win.metaAttributesCache.set('ol-groupSSOActive', true)
+      })
+    })
+
+    describe('with an ordinary user', function () {
+      let user: User
+
+      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,
+        }
+        cy.window().then(win => {
+          win.metaAttributesCache.set('ol-users', [user])
+        })
+
+        cy.mount(
+          <GroupMembersProvider>
+            <MemberRow
+              user={user}
+              openOffboardingModalForUser={sinon.stub()}
+              groupId={subscriptionId}
+              setManagedUserAlert={sinon.stub()}
+            />
+          </GroupMembersProvider>
+        )
+      })
+
+      it('renders the row', function () {
+        cy.get('tr').should('exist')
+        // Checkbox
+        cy.get('.select-item').should('not.be.checked')
+        // Email
+        cy.get('tr').contains(user.email)
+        // Name
+        cy.get('tr').contains(user.first_name)
+        cy.get('tr').contains(user.last_name)
+        // Last active date
+        cy.get('tr').contains('21st Nov 2070')
+        // Managed status
+        cy.get('tr').contains('Managed')
+        // SSO status
+        cy.get('tr').contains('SSO')
+        // Dropdown button
+        cy.get('#managed-user-dropdown-some\\.user\\@example\\.com').should(
+          'exist'
+        )
+      })
+    })
+
+    describe('with a pending invite', function () {
+      let user: User
+
+      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,
+        }
+        cy.window().then(win => {
+          win.metaAttributesCache.set('ol-users', [user])
+        })
+
+        cy.mount(
+          <GroupMembersProvider>
+            <MemberRow
+              user={user}
+              openOffboardingModalForUser={sinon.stub()}
+              groupId={subscriptionId}
+              setManagedUserAlert={sinon.stub()}
+            />
+          </GroupMembersProvider>
+        )
+      })
+
+      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
+
+      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,
+        }
+        cy.window().then(win => {
+          win.metaAttributesCache.set('ol-users', [user])
+        })
+
+        cy.mount(
+          <GroupMembersProvider>
+            <MemberRow
+              user={user}
+              openOffboardingModalForUser={sinon.stub()}
+              groupId={subscriptionId}
+              setManagedUserAlert={sinon.stub()}
+            />
+          </GroupMembersProvider>
+        )
+      })
+
+      it('should render a "Group admin" symbol', function () {
+        cy.get('[aria-label="Group admin"].fa-user-circle-o').should('exist')
+      })
+    })
+
+    describe('selecting and unselecting user row', function () {
+      let user: User
+
+      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,
+        }
+        cy.window().then(win => {
+          win.metaAttributesCache.set('ol-users', [user])
+        })
+
+        cy.mount(
+          <GroupMembersProvider>
+            <MemberRow
+              user={user}
+              openOffboardingModalForUser={sinon.stub()}
+              groupId={subscriptionId}
+              setManagedUserAlert={sinon.stub()}
+            />
+          </GroupMembersProvider>
+        )
+      })
+
+      it('should select and unselect the user', function () {
+        cy.get('.select-item').should('not.be.checked')
+        cy.get('.select-item').click()
+        cy.get('.select-item').should('be.checked')
+        cy.get('.select-item').click()
+        cy.get('.select-item').should('not.be.checked')
+      })
+    })
+  })
+})

+ 5 - 5
services/web/test/frontend/features/group-management/components/managed-users/managed-users-list.spec.tsx → services/web/test/frontend/features/group-management/components/members-table/members-list.spec.tsx

@@ -1,17 +1,17 @@
-import ManagedUsersList from '../../../../../../frontend/js/features/group-management/components/managed-users/managed-users-list'
-import { GroupMembersProvider } from '../../../../../../frontend/js/features/group-management/context/group-members-context'
+import MembersList from '@/features/group-management/components/members-table/members-list'
+import { GroupMembersProvider } from '@/features/group-management/context/group-members-context'
 
 const groupId = 'somegroup'
 
 function mountManagedUsersList() {
   cy.mount(
     <GroupMembersProvider>
-      <ManagedUsersList groupId={groupId} />
+      <MembersList groupId={groupId} />
     </GroupMembersProvider>
   )
 }
 
-describe('ManagedUsersList', function () {
+describe('MembersList', function () {
   describe('with users', function () {
     const users = [
       {
@@ -98,7 +98,7 @@ describe('ManagedUsersList', function () {
       })
       cy.mount(
         <GroupMembersProvider>
-          <ManagedUsersList groupId={groupId} />
+          <MembersList groupId={groupId} />
         </GroupMembersProvider>
       )
     })

+ 1 - 1
services/web/test/frontend/features/group-management/components/managed-users/offboard-managed-user-modal.spec.tsx → services/web/test/frontend/features/group-management/components/members-table/offboard-managed-user-modal.spec.tsx

@@ -1,4 +1,4 @@
-import OffboardManagedUserModal from '../../../../../../frontend/js/features/group-management/components/managed-users/offboard-managed-user-modal'
+import OffboardManagedUserModal from '@/features/group-management/components/members-table/offboard-managed-user-modal'
 import sinon from 'sinon'
 
 describe('OffboardManagedUserModal', function () {

+ 1 - 1
services/web/test/frontend/features/group-management/components/publisher-managers.spec.tsx

@@ -1,4 +1,4 @@
-import PublisherManagers from '../../../../../frontend/js/features/group-management/components/publisher-managers'
+import PublisherManagers from '@/features/group-management/components/publisher-managers'
 
 const JOHN_DOE = {
   _id: 'abc123def456',