From a9ddf24343bb69e51c82e5380b5e69323aea8aa9 Mon Sep 17 00:00:00 2001 From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> Date: Wed, 26 Feb 2025 11:43:01 +0200 Subject: [PATCH] Merge pull request #23743 from overleaf/ii-bs5-manage-group-members [web] BS5 Group members management GitOrigin-RevId: fab24ee6f6de07aa64887e123df930593fcec6a2 --- .../UserMembershipController.mjs | 2 + .../user_membership/group-members-react.pug | 4 + .../components/back-button.tsx | 34 ++ .../components/error-alert.tsx | 12 +- .../components/group-members.tsx | 129 ++++---- .../members-table/dropdown-button.tsx | 164 +++++++--- .../components/members-table/list-alert.tsx | 270 ++++++++-------- .../components/members-table/member-row.tsx | 93 ++++-- .../components/members-table/members-list.tsx | 125 ++++--- .../offboard-managed-user-modal.tsx | 108 +++---- .../members-table/select-all-checkbox.tsx | 37 ++- .../members-table/select-user-checkbox.tsx | 39 ++- .../members-table/unlink-user-modal.tsx | 37 ++- .../js/features/ui/components/ol/ol-card.tsx | 19 ++ .../js/features/ui/components/ol/ol-table.tsx | 3 + .../components/notification-scrolled-to.tsx | 6 +- .../stylesheets/app/subscription.less | 19 ++ .../bootstrap-5/components/all.scss | 1 + .../bootstrap-5/components/group-members.scss | 161 ++++++++++ .../bootstrap-5/components/table.scss | 8 + .../bootstrap-5/pages/project-list.scss | 6 - .../bootstrap-5/pages/subscription.scss | 3 +- .../stylesheets/components/group-members.less | 97 +++--- .../components/group-members.spec.tsx | 304 ++++++++++-------- .../components/managed-group-members.spec.tsx | 196 ++++++----- .../members-table/member-row.spec.tsx | 68 ++-- .../members-table/members-list.spec.tsx | 192 +++++++---- 27 files changed, 1314 insertions(+), 823 deletions(-) create mode 100644 services/web/frontend/js/features/group-management/components/back-button.tsx create mode 100644 services/web/frontend/js/features/ui/components/ol/ol-card.tsx create mode 100644 services/web/frontend/stylesheets/bootstrap-5/components/group-members.scss diff --git a/services/web/app/src/Features/UserMembership/UserMembershipController.mjs b/services/web/app/src/Features/UserMembership/UserMembershipController.mjs index e17793ab0f..3fa0f57a74 100644 --- a/services/web/app/src/Features/UserMembership/UserMembershipController.mjs +++ b/services/web/app/src/Features/UserMembership/UserMembershipController.mjs @@ -38,6 +38,8 @@ async function manageGroupMembers(req, res, next) { 'flexible-group-licensing' ) + await SplitTestHandler.promises.getAssignment(req, res, 'bootstrap-5-groups') + const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode) const userId = SessionManager.getLoggedInUserId(req.session) const isAdmin = subscription.admin_id.toString() === userId diff --git a/services/web/app/views/user_membership/group-members-react.pug b/services/web/app/views/user_membership/group-members-react.pug index b38c118ba7..5a3b4f45f2 100644 --- a/services/web/app/views/user_membership/group-members-react.pug +++ b/services/web/app/views/user_membership/group-members-react.pug @@ -2,6 +2,10 @@ extends ../layout-marketing block entrypointVar - entrypoint = 'pages/user/subscription/group-management/group-members' + +block vars + - bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly' + - bootstrap5PageSplitTest = 'bootstrap-5-groups' block append meta meta(name="ol-users", data-type="json", content=users) diff --git a/services/web/frontend/js/features/group-management/components/back-button.tsx b/services/web/frontend/js/features/group-management/components/back-button.tsx new file mode 100644 index 0000000000..5efac75366 --- /dev/null +++ b/services/web/frontend/js/features/group-management/components/back-button.tsx @@ -0,0 +1,34 @@ +import MaterialIcon from '@/shared/components/material-icon' +import IconButton from '@/features/ui/components/bootstrap-5/icon-button' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' + +type BackButtonProps = { + href: string + accessibilityLabel: string +} + +function BackButton({ href, accessibilityLabel }: BackButtonProps) { + return ( + + + + } + bs5={ + + } + /> + ) +} + +export default BackButton diff --git a/services/web/frontend/js/features/group-management/components/error-alert.tsx b/services/web/frontend/js/features/group-management/components/error-alert.tsx index a7f971dba5..e96ded5d7c 100644 --- a/services/web/frontend/js/features/group-management/components/error-alert.tsx +++ b/services/web/frontend/js/features/group-management/components/error-alert.tsx @@ -1,4 +1,5 @@ import { useTranslation } from 'react-i18next' +import OLNotification from '@/features/ui/components/ol/ol-notification' export type APIError = { message?: string @@ -17,15 +18,14 @@ export default function ErrorAlert({ error }: ErrorAlertProps) { if (error.message) { return ( -
- {t('error')}: {error.message} -
+ ) } return ( -
- {t('generic_something_went_wrong')} -
+ ) } diff --git a/services/web/frontend/js/features/group-management/components/group-members.tsx b/services/web/frontend/js/features/group-management/components/group-members.tsx index bce64164c7..3fb36f7688 100644 --- a/services/web/frontend/js/features/group-management/components/group-members.tsx +++ b/services/web/frontend/js/features/group-management/components/group-members.tsx @@ -1,7 +1,5 @@ import React, { useCallback, useState } from 'react' -import { Button, Col, Form, FormControl, Row } from 'react-bootstrap' import { Trans, useTranslation } from 'react-i18next' -import MaterialIcon from '../../../shared/components/material-icon' import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n' import getMeta from '../../../utils/meta' import { useGroupMembersContext } from '../context/group-members-context' @@ -9,6 +7,13 @@ import ErrorAlert from './error-alert' import MembersList from './members-table/members-list' import { useFeatureFlag } from '@/shared/context/split-test-context' import { sendMB } from '../../../infrastructure/event-tracking' +import BackButton from '@/features/group-management/components/back-button' +import OLRow from '@/features/ui/components/ol/ol-row' +import OLCol from '@/features/ui/components/ol/ol-col' +import OLCard from '@/features/ui/components/ol/ol-card' +import OLButton from '@/features/ui/components/ol/ol-button' +import OLFormControl from '@/features/ui/components/ol/ol-form-control' +import OLFormText from '@/features/ui/components/ol/ol-form-text' export default function GroupMembers() { const { isReady } = useWaitForI18n() @@ -48,7 +53,7 @@ export default function GroupMembers() { return null } - const onAddMembersSubmit = (e: React.FormEvent
) => { + const onAddMembersSubmit = (e: React.FormEvent) => { e.preventDefault() addMembers(emailString) } @@ -98,36 +103,37 @@ export default function GroupMembers() { return (
- - -

- - - {' '} - {groupName || t('group_subscription')} -

-
-
+ + +
+ +

{groupName || t('group_subscription')}

+
+ +
{selectedUsers.length === 0 && groupSizeDetails()} {removeMemberLoading ? ( - + ) : ( <> {selectedUsers.length > 0 && ( - + )} )}
-

{t('members_management')}

+

{t('members_management')}

@@ -145,58 +151,67 @@ export default function GroupMembers() { : t('add_more_members')}

- - - - + + + - - - {inviteMemberLoading ? ( - - ) : ( - - )} - - + + + + {isFlexibleGroupLicensing + ? t('inviting') + : t('adding')} + … + + ) : isFlexibleGroupLicensing ? ( + t('invite') + ) : ( + t('add') + ), + }} + > + {isFlexibleGroupLicensing ? t('invite') : t('add')} + + + {t('export_csv')} - - - - - + + + + + {t('add_comma_separated_emails_help')} - - - - + + + +
)} {users.length >= groupSize && users.length > 0 && ( <> - - + + {t('export_csv')} - - +
+
)} -
- - + + +
) } diff --git a/services/web/frontend/js/features/group-management/components/members-table/dropdown-button.tsx b/services/web/frontend/js/features/group-management/components/members-table/dropdown-button.tsx index dcd766d643..284e31a860 100644 --- a/services/web/frontend/js/features/group-management/components/members-table/dropdown-button.tsx +++ b/services/web/frontend/js/features/group-management/components/members-table/dropdown-button.tsx @@ -6,7 +6,13 @@ import { type SetStateAction, } from 'react' import { useTranslation } from 'react-i18next' -import { Dropdown, MenuItem } from 'react-bootstrap' +import { Dropdown as BS3Dropdown, MenuItem } from 'react-bootstrap' +import { + Dropdown, + DropdownItem, + DropdownMenu, + DropdownToggle, +} from '@/features/ui/components/bootstrap-5/dropdown-menu' import { User } from '../../../../../../types/group-management/user' import useAsync from '@/shared/hooks/use-async' import { type FetchError, postJSON } from '@/infrastructure/fetch-json' @@ -14,6 +20,10 @@ import Icon from '@/shared/components/icon' import { GroupUserAlert } from '../../utils/types' import { useGroupMembersContext } from '../../context/group-members-context' import getMeta from '@/utils/meta' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import MaterialIcon from '@/shared/components/material-icon' +import DropdownListItem from '@/features/ui/components/bootstrap-5/dropdown-list-item' +import { Spinner } from 'react-bootstrap-5' type resendInviteResponse = { success: boolean @@ -191,12 +201,10 @@ export default function DropdownButton({ {t('resend_group_invite')} - {isResendingGroupInvite ? ( - - ) : null} ) } @@ -205,12 +213,10 @@ export default function DropdownButton({ {t('resend_managed_user_invite')} - {isResendingManagedUserInvite ? ( - - ) : null} ) } @@ -230,12 +236,10 @@ export default function DropdownButton({ {t('resend_link_sso')} - {isResendingSSOLinkInvite ? ( - - ) : null} ) } @@ -257,6 +261,7 @@ export default function DropdownButton({ data-testid="remove-user-action" onClick={onRemoveFromGroup} className="delete-user-action" + variant="danger" > {t('remove_from_group')} @@ -265,54 +270,119 @@ export default function DropdownButton({ if (buttons.length === 0) { buttons.push( - - {t('no_actions')} - + + {t('no_actions')} + + } + bs5={ + + + {t('no_actions')} + + + } + /> ) } return ( - - setIsOpened(open)} - > - -