From 1e4028d05e3d5f2c3da7ea34ff83a9eebe29dcd9 Mon Sep 17 00:00:00 2001 From: M Fahru Date: Tue, 22 Aug 2023 14:38:37 -0700 Subject: [PATCH] Merge pull request #14311 from overleaf/mf-resend-surrender-email [web] Add an option to resend managed users invite in managed users setting GitOrigin-RevId: 2734ef3be31f77c309caec96e97411c9d48a8160 --- .../app/src/Features/Email/EmailBuilder.js | 12 +- .../web/frontend/extracted-translations.json | 4 + .../managed-user-dropdown-button.tsx | 111 +++++++++++++- .../managed-users/managed-user-row.tsx | 13 +- .../managed-users-list-alert.tsx | 139 ++++++++++++++++++ .../managed-users/managed-users-list.tsx | 15 +- .../features/group-management/utils/types.ts | 11 ++ .../stylesheets/app/project-list.less | 2 +- .../stylesheets/components/group-members.less | 45 +++++- services/web/locales/en.json | 5 + .../managed-group-members.spec.tsx | 22 +-- .../managed-user-dropdown-button.spec.tsx | 8 + .../managed-users/managed-user-row.spec.tsx | 10 ++ 13 files changed, 366 insertions(+), 31 deletions(-) create mode 100644 services/web/frontend/js/features/group-management/components/managed-users/managed-users-list-alert.tsx create mode 100644 services/web/frontend/js/features/group-management/utils/types.ts diff --git a/services/web/app/src/Features/Email/EmailBuilder.js b/services/web/app/src/Features/Email/EmailBuilder.js index b3dc813c51..3af9c703e7 100644 --- a/services/web/app/src/Features/Email/EmailBuilder.js +++ b/services/web/app/src/Features/Email/EmailBuilder.js @@ -443,14 +443,22 @@ templates.surrenderAccountForManagedUsers = ctaTemplate({ const toGroupName = opts.groupName ? ` to ${opts.groupName}` : '' - return `You’ve been invited by ${admin} to transfer management of your ${settings.appName} account${toGroupName}` + return `${ + opts.reminder ? 'Reminder: ' : '' + }You’ve been invited by ${admin} to transfer management of your ${ + settings.appName + } account${toGroupName}` }, title(opts) { const admin = _.escape(_formatUserNameAndEmail(opts.admin, 'an admin')) const toGroupName = opts.groupName ? ` to ${opts.groupName}` : '' - return `You’ve been invited by ${admin} to transfer management of your ${settings.appName} account${toGroupName}` + return `${ + opts.reminder ? 'Reminder: ' : '' + }You’ve been invited by ${admin} to transfer management of your ${ + settings.appName + } account${toGroupName}` }, message(opts, isPlainText) { const admin = _.escape(_formatUserNameAndEmail(opts.admin, 'an admin')) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 971daa0bdb..6a91815ce1 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -59,6 +59,7 @@ "all_projects": "", "all_projects_will_be_transferred_immediately": "", "also": "", + "an_email_has_already_been_sent_to": "", "an_error_occurred_when_verifying_the_coupon_code": "", "anonymous": "", "anyone_with_link_can_edit": "", @@ -304,6 +305,7 @@ "expires": "", "export_csv": "", "export_project_to_github": "", + "failed_to_send_managed_user_invite_to_email": "", "fast": "", "faster_compiles_feedback_question": "", "faster_compiles_feedback_seems_faster": "", @@ -589,6 +591,7 @@ "manage_sessions": "", "manage_subscription": "", "managed": "", + "managed_user_invite_has_been_sent_to_email": "", "managed_users": "", "managed_users_explanation": "", "managed_users_terms": "", @@ -841,6 +844,7 @@ "resend": "", "resend_confirmation_email": "", "resend_email": "", + "resend_managed_user_invite": "", "resending_confirmation_email": "", "resolve": "", "resolved_comments": "", diff --git a/services/web/frontend/js/features/group-management/components/managed-users/managed-user-dropdown-button.tsx b/services/web/frontend/js/features/group-management/components/managed-users/managed-user-dropdown-button.tsx index a7db27e0a7..b431b0cd6c 100644 --- a/services/web/frontend/js/features/group-management/components/managed-users/managed-user-dropdown-button.tsx +++ b/services/web/frontend/js/features/group-management/components/managed-users/managed-user-dropdown-button.tsx @@ -1,21 +1,88 @@ +import { + useState, + type ComponentProps, + useCallback, + type Dispatch, + type SetStateAction, +} from 'react' +import { useTranslation } from 'react-i18next' import { Dropdown, MenuItem } from 'react-bootstrap' import { User } from '../../../../../../types/group-management/user' -import ControlledDropdown from '../../../../shared/components/controlled-dropdown' -import { useTranslation } from 'react-i18next' -import MenuItemButton from '../../../project-list/components/dropdown/menu-item-button' +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' +type ResendManagedUserInviteResponse = { + success: boolean +} + type ManagedUserDropdownButtonProps = { user: User openOffboardingModalForUser: (user: User) => void + groupId: string + setManagedUserAlert: Dispatch> } export default function ManagedUserDropdownButton({ user, openOffboardingModalForUser, + groupId, + setManagedUserAlert, }: ManagedUserDropdownButtonProps) { const { t } = useTranslation() const { removeMember } = useGroupMembersContext() + const [isOpened, setIsOpened] = useState(false) + const { + runAsync: runResendManagedUserInviteAsync, + isLoading: isResendingManagedUserInvite, + } = useAsync() + + const userNotManaged = + !user.isEntityAdmin && !user.invite && !user.enrollment?.managedBy + + const handleResendManagedUserInvite = useCallback( + async user => { + try { + const result = await runResendManagedUserInviteAsync( + postJSON( + `/manage/groups/${groupId}/resendManagedUserInvite/${user._id}` + ) + ) + + if (result.success) { + setManagedUserAlert({ + variant: 'resendManagedUserInviteSuccess', + email: user.email, + }) + setIsOpened(false) + } + } catch (err) { + if ((err as FetchError)?.response?.status === 429) { + setManagedUserAlert({ + variant: 'resendManagedUserInviteTooManyRequests', + email: user.email, + }) + } else { + setManagedUserAlert({ + variant: 'resendManagedUserInviteFailed', + email: user.email, + }) + } + + setIsOpened(false) + } + }, + [setManagedUserAlert, groupId, runResendManagedUserInviteAsync] + ) + + const onResendManagedUserInviteClick = () => { + handleResendManagedUserInvite(user) + } const onDeleteUserClick = () => { openOffboardingModalForUser(user) @@ -27,7 +94,11 @@ export default function ManagedUserDropdownButton({ return ( - + setIsOpened(open)} + > - + + {userNotManaged ? ( + + {t('resend_managed_user_invite')} + {isResendingManagedUserInvite ? ( + + ) : null} + + ) : null} {user.enrollment ? ( )} - + ) } + +function MenuItemButton({ + children, + onClick, + className, + ...buttonProps +}: ComponentProps<'button'>) { + return ( +
  • + +
  • + ) +} diff --git a/services/web/frontend/js/features/group-management/components/managed-users/managed-user-row.tsx b/services/web/frontend/js/features/group-management/components/managed-users/managed-user-row.tsx index e709fb1e87..d49aae0ed3 100644 --- a/services/web/frontend/js/features/group-management/components/managed-users/managed-user-row.tsx +++ b/services/web/frontend/js/features/group-management/components/managed-users/managed-user-row.tsx @@ -1,22 +1,27 @@ import moment from 'moment' -import { useCallback } from 'react' +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' -import { useGroupMembersContext } from '../../context/group-members-context' -import ManagedUserDropdownButton from './managed-user-dropdown-button' import Tooltip from '../../../../shared/components/tooltip' +import type { ManagedUserAlert } from '../../utils/types' +import { useGroupMembersContext } from '../../context/group-members-context' import ManagedUserStatus from './managed-user-status' +import ManagedUserDropdownButton from './managed-user-dropdown-button' type ManagedUserRowProps = { user: User openOffboardingModalForUser: (user: User) => void + groupId: string + setManagedUserAlert: Dispatch> } export default function ManagedUserRow({ user, openOffboardingModalForUser, + setManagedUserAlert, + groupId, }: ManagedUserRowProps) { const { t } = useTranslation() const { selectedUsers, selectUser, unselectUser } = useGroupMembersContext() @@ -101,6 +106,8 @@ export default function ManagedUserRow({ diff --git a/services/web/frontend/js/features/group-management/components/managed-users/managed-users-list-alert.tsx b/services/web/frontend/js/features/group-management/components/managed-users/managed-users-list-alert.tsx new file mode 100644 index 0000000000..4673e6b5b1 --- /dev/null +++ b/services/web/frontend/js/features/group-management/components/managed-users/managed-users-list-alert.tsx @@ -0,0 +1,139 @@ +import { type PropsWithChildren, useState } from 'react' +import { Alert, type AlertProps } from 'react-bootstrap' +import { Trans, useTranslation } from 'react-i18next' +import type { ManagedUserAlertVariant } from '../../utils/types' + +type ManagedUsersListAlertProps = { + variant: ManagedUserAlertVariant + invitedUserEmail?: string + onDismiss: () => void +} + +export default function ManagedUsersListAlert({ + variant, + invitedUserEmail, + onDismiss, +}: ManagedUsersListAlertProps) { + switch (variant) { + case 'resendManagedUserInviteSuccess': + return ( + + ) + case 'resendManagedUserInviteFailed': + return ( + + ) + case 'resendManagedUserInviteTooManyRequests': + return ( + + ) + } +} + +type ManagedUsersListAlertComponentProps = { + onDismiss: () => void + invitedUserEmail?: string +} + +function ResendManagedUserInviteSuccess({ + onDismiss, + invitedUserEmail, +}: ManagedUsersListAlertComponentProps) { + return ( + + , + ]} + /> + + ) +} + +function FailedToResendManagedInvite({ + onDismiss, + invitedUserEmail, +}: ManagedUsersListAlertComponentProps) { + return ( + + , + ]} + /> + + ) +} + +function TooManyRequests({ + onDismiss, + invitedUserEmail, +}: ManagedUsersListAlertComponentProps) { + return ( + + , + ]} + /> + + ) +} + +type AlertComponentProps = PropsWithChildren<{ + bsStyle: AlertProps['bsStyle'] + onDismiss: AlertProps['onDismiss'] +}> + +function AlertComponent({ bsStyle, onDismiss, children }: AlertComponentProps) { + const [show, setShow] = useState(true) + const { t } = useTranslation() + + const handleDismiss = () => { + if (onDismiss) { + onDismiss() + } + + setShow(false) + } + + if (!show) { + return null + } + + return ( + + {children} +
    + +
    +
    + ) +} diff --git a/services/web/frontend/js/features/group-management/components/managed-users/managed-users-list.tsx b/services/web/frontend/js/features/group-management/components/managed-users/managed-users-list.tsx index 1c6c2ad715..39de4cb7da 100644 --- a/services/web/frontend/js/features/group-management/components/managed-users/managed-users-list.tsx +++ b/services/web/frontend/js/features/group-management/components/managed-users/managed-users-list.tsx @@ -1,11 +1,13 @@ +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 { useGroupMembersContext } from '../../context/group-members-context' +import type { ManagedUserAlert } from '../../utils/types' import ManagedUserRow from './managed-user-row' import OffboardManagedUserModal from './offboard-managed-user-modal' -import { useState } from 'react' +import ManagedUsersListAlert from './managed-users-list-alert' type ManagedUsersListProps = { handleSelectAllClick: (e: any) => void @@ -20,10 +22,19 @@ export default function ManagedUsersList({ const [userToOffboard, setUserToOffboard] = useState( undefined ) + const [managedUserAlert, setManagedUserAlert] = + useState(undefined) const { selectedUsers, users } = useGroupMembersContext() return (
    + {managedUserAlert && ( + setManagedUserAlert(undefined)} + /> + )}
    • @@ -76,6 +87,8 @@ export default function ManagedUsersList({ key={user.email} user={user} openOffboardingModalForUser={setUserToOffboard} + setManagedUserAlert={setManagedUserAlert} + groupId={groupId} /> ))}
    diff --git a/services/web/frontend/js/features/group-management/utils/types.ts b/services/web/frontend/js/features/group-management/utils/types.ts new file mode 100644 index 0000000000..f2817e6d5a --- /dev/null +++ b/services/web/frontend/js/features/group-management/utils/types.ts @@ -0,0 +1,11 @@ +export type ManagedUserAlertVariant = + | 'resendManagedUserInviteSuccess' + | 'resendManagedUserInviteFailed' + | 'resendManagedUserInviteTooManyRequests' + +export type ManagedUserAlert = + | { + variant: ManagedUserAlertVariant + email?: string + } + | undefined diff --git a/services/web/frontend/stylesheets/app/project-list.less b/services/web/frontend/stylesheets/app/project-list.less index ad37c1961d..8e6ec67bfb 100644 --- a/services/web/frontend/stylesheets/app/project-list.less +++ b/services/web/frontend/stylesheets/app/project-list.less @@ -437,7 +437,7 @@ ul.structured-list { overflow: hidden; overflow-y: auto; -ms-overflow-style: -ms-autohiding-scrollbar; - li { + > li { border-bottom: 1px solid @structured-list-border-color; padding: (@line-height-computed / 4) 0; diff --git a/services/web/frontend/stylesheets/components/group-members.less b/services/web/frontend/stylesheets/components/group-members.less index 0651ed9ec3..0b6b8d5d93 100644 --- a/services/web/frontend/stylesheets/components/group-members.less +++ b/services/web/frontend/stylesheets/components/group-members.less @@ -20,6 +20,7 @@ .managed-user-row { overflow-wrap: break-word; } + .managed-user-security { display: flex; justify-content: space-between; @@ -30,15 +31,45 @@ padding-left: 1em; padding-right: 1em; } - li > button { - &:hover { - background-color: @gray-lightest; + .managed-user-dropdown-menu { + width: 300px; + + li > button { + &:hover { + background-color: @gray-lightest; + } } - } - .delete-user-action { - button { - color: @red; + .delete-user-action { + button { + color: @red; + } } } } + .managed-user-menu-item-button { + padding: 12px 20px; + position: relative; + width: 100%; + border: none; + box-shadow: none; + background: inherit; + color: @ol-blue-gray-5; + text-align: left; + &[disabled] { + background-color: @gray-lighter; + } + } +} + +.managed-users-list-alert { + display: flex; + justify-content: space-between; + .managed-users-list-alert-close { + padding-left: @padding-sm; + text-align: right; + width: 10%; + @media (min-width: @screen-sm-min) { + width: auto; + } + } } diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 4341ebc09a..d7fcbc5b1a 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -95,6 +95,7 @@ "also": "Also", "also_available_as_on_premises": "Also available as On-Premises", "alternatively_create_new_institution_account": "Alternatively, you can create a new account with your institution email (__email__) by clicking __clickText__.", + "an_email_has_already_been_sent_to": "An email has already been sent to <0>__email__. Please wait and try again later.", "an_error_occurred_when_verifying_the_coupon_code": "An error occurred when verifying the coupon code", "and": "and", "annual": "Annual", @@ -484,6 +485,7 @@ "expiry": "Expiry Date", "export_csv": "Export CSV", "export_project_to_github": "Export Project to GitHub", + "failed_to_send_managed_user_invite_to_email": "Failed to send Managed User invite to <0>__email__. Please try again later.", "faq_change_plans_or_cancel_answer": "Yes, you can do this at any time via your subscription settings. You can change plans, switch between monthly and annual billing options, or cancel to downgrade to the free plan. When cancelling, your subscription will continue until the end of the billing period. If your account temporarily does not have a subscription, the only change will be to the features available to you. Your projects will always be available on your account.", "faq_change_plans_or_cancel_question": "Can I change plans or cancel later?", "faq_do_collab_need_on_paid_plan_answer": "No, they can be on any plan, including the free plan. If you are on a premium plan, some premium features will be available to your collaborators in projects that you have created, even if those collaborators are on the free plan. For more information, read about <0>account and subscriptions and <1>how premium features work.", @@ -968,6 +970,7 @@ "manage_sessions": "Manage Your Sessions", "manage_subscription": "Manage Subscription", "managed": "Managed", + "managed_user_invite_has_been_sent_to_email": "Managed User invite has been sent to <0>__email__", "managed_users": "Managed Users", "managed_users_explanation": "Managed Users ensures you stay in control of your organization’s projects and who owns them. <0>Read more about Managed Users.", "managed_users_terms": "To use the Managed Users feature, you must agree to the latest version of our customer terms at <0>__link__ on behalf of your organization by selecting \"I agree\" below. These terms will then apply to your organization’s use of Overleaf in place of any previously agreed Overleaf terms, except where we have a signed agreement in place with you, in which case that signed agreement will continue to govern. Please keep a copy for your records", @@ -1376,6 +1379,7 @@ "resend": "Resend", "resend_confirmation_email": "Resend confirmation email", "resend_email": "Resend email", + "resend_managed_user_invite": "Resend Managed User invite", "resending_confirmation_email": "Resending confirmation email", "reset_password": "Reset Password", "reset_your_password": "Reset your password", @@ -1809,6 +1813,7 @@ "user_already_added": "User already added", "user_deletion_error": "Sorry, something went wrong deleting your account. Please try again in a minute.", "user_deletion_password_reset_tip": "If you cannot remember your password, or if you are using Single-Sign-On with another provider to sign in (such as Twitter or Google), please <0>reset your password and try again.", + "user_is_not_part_of_group": "User is not part of group", "user_management": "User management", "user_management_info": "Group plan admins have access to an admin panel where users can be added and removed easily. For site-wide plans, users are automatically upgraded when they register or add their email address to Overleaf (domain-based enrollment or SSO).", "user_not_found": "User not found", diff --git a/services/web/test/frontend/features/group-management/components/managed-users/managed-group-members.spec.tsx b/services/web/test/frontend/features/group-management/components/managed-users/managed-group-members.spec.tsx index 53be7ab01c..5fcd27b0dc 100644 --- a/services/web/test/frontend/features/group-management/components/managed-users/managed-group-members.spec.tsx +++ b/services/web/test/frontend/features/group-management/components/managed-users/managed-group-members.spec.tsx @@ -64,7 +64,7 @@ describe('group members, with managed users', function () { 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('> li:nth-child(2)').within(() => { cy.contains('john.doe@test.com') cy.contains('John Doe') cy.contains('15th Jan 2023') @@ -74,7 +74,7 @@ describe('group members, with managed users', function () { cy.get(`.security-state-invite-pending`).should('exist') }) - cy.get('li:nth-child(3)').within(() => { + cy.get('> li:nth-child(3)').within(() => { cy.contains('bobby.lapointe@test.com') cy.contains('Bobby Lapointe') cy.contains('2nd Jan 2023') @@ -82,7 +82,7 @@ describe('group members, with managed users', function () { cy.get('i[aria-label="Not managed"]').should('exist') }) - cy.get('li:nth-child(4)').within(() => { + cy.get('> li:nth-child(4)').within(() => { cy.contains('claire.jennings@test.com') cy.contains('Claire Jennings') cy.contains('3rd Jan 2023') @@ -107,7 +107,7 @@ describe('group members, with managed users', function () { cy.get('.add-more-members-form button').click() cy.get('ul.managed-users-list').within(() => { - cy.get('li:nth-child(5)').within(() => { + cy.get('> li:nth-child(5)').within(() => { cy.contains('someone.else@test.com') cy.contains('N/A') cy.get(`[aria-label="Pending invite"]`) @@ -134,7 +134,7 @@ 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('> li:nth-child(2)').within(() => { cy.get('.select-item').should('not.be.checked') }) cy.get('li:nth-child(3)').within(() => { @@ -145,7 +145,7 @@ describe('group members, with managed users', function () { cy.get('.select-all').click() cy.get('ul.managed-users-list').within(() => { - cy.get('li:nth-child(2)').within(() => { + cy.get('> li:nth-child(2)').within(() => { cy.get('.select-item').should('be.checked') }) cy.get('li:nth-child(3)').within(() => { @@ -160,7 +160,7 @@ describe('group members, with managed users', function () { }) cy.get('ul.managed-users-list').within(() => { - cy.get('li:nth-child(2)').within(() => { + cy.get('> li:nth-child(2)').within(() => { cy.get('.select-item').check() }) }) @@ -169,7 +169,7 @@ describe('group members, with managed users', function () { 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('> li:nth-child(2)').within(() => { cy.contains('bobby.lapointe@test.com') cy.contains('Bobby Lapointe') cy.contains('2nd Jan 2023') @@ -184,7 +184,7 @@ describe('group members, with managed users', function () { cy.get('ul.managed-users-list').within(() => { // Select 'Claire Jennings', a managed user - cy.get('li:nth-child(4)').within(() => { + cy.get('> li:nth-child(4)').within(() => { cy.get('.select-item').check() }) }) @@ -201,7 +201,7 @@ describe('group members, with managed users', function () { cy.get('ul.managed-users-list').within(() => { // Select 'Claire Jennings', a managed user - cy.get('li:nth-child(4)').within(() => { + cy.get('> li:nth-child(4)').within(() => { cy.get('.select-item').check() }) // Select another user @@ -221,7 +221,7 @@ describe('group members, with managed users', function () { }) cy.get('ul.managed-users-list').within(() => { - cy.get('li:nth-child(2)').within(() => { + cy.get('> li:nth-child(2)').within(() => { cy.get('.select-item').check() }) }) diff --git a/services/web/test/frontend/features/group-management/components/managed-users/managed-user-dropdown-button.spec.tsx b/services/web/test/frontend/features/group-management/components/managed-users/managed-user-dropdown-button.spec.tsx index 440e87154f..1b010ca137 100644 --- a/services/web/test/frontend/features/group-management/components/managed-users/managed-user-dropdown-button.spec.tsx +++ b/services/web/test/frontend/features/group-management/components/managed-users/managed-user-dropdown-button.spec.tsx @@ -3,6 +3,8 @@ import sinon from 'sinon' import { GroupMembersProvider } from '../../../../../../frontend/js/features/group-management/context/group-members-context' describe('ManagedUserDropdownButton', function () { + const subscriptionId = '123abc' + describe('with managed user', function () { const user = { _id: 'some-user', @@ -28,6 +30,8 @@ describe('ManagedUserDropdownButton', function () { ) @@ -71,6 +75,8 @@ describe('ManagedUserDropdownButton', function () { ) @@ -114,6 +120,8 @@ describe('ManagedUserDropdownButton', function () { ) diff --git a/services/web/test/frontend/features/group-management/components/managed-users/managed-user-row.spec.tsx b/services/web/test/frontend/features/group-management/components/managed-users/managed-user-row.spec.tsx index 49cf78d528..fa31fd687a 100644 --- a/services/web/test/frontend/features/group-management/components/managed-users/managed-user-row.spec.tsx +++ b/services/web/test/frontend/features/group-management/components/managed-users/managed-user-row.spec.tsx @@ -4,6 +4,8 @@ import { GroupMembersProvider } from '../../../../../../frontend/js/features/gro import { User } from '../../../../../../types/group-management/user' describe('ManagedUserRow', function () { + const subscriptionId = '123abc' + describe('with an ordinary user', function () { let user: User @@ -27,6 +29,8 @@ describe('ManagedUserRow', function () { ) @@ -75,6 +79,8 @@ describe('ManagedUserRow', function () { ) @@ -108,6 +114,8 @@ describe('ManagedUserRow', function () { ) @@ -141,6 +149,8 @@ describe('ManagedUserRow', function () { )