From a14e2aecfbd4fc495967395a8943939575567c80 Mon Sep 17 00:00:00 2001 From: June Kelly Date: Fri, 30 Jun 2023 09:30:20 +0100 Subject: [PATCH] Merge pull request #13483 from overleaf/jk-managed-users-group-management-ui [web] Managed Users: Update Group Member Management UI GitOrigin-RevId: 6896951927f0e3220db59dda208d7cfe9c6c309e --- .../UserMembershipController.js | 7 + .../UserMembership/UserMembershipHandler.js | 16 +- .../UserMembership/UserMembershipViewModel.js | 7 + .../user_membership/group-members-react.pug | 1 + services/web/config/settings.defaults.js | 4 + .../web/frontend/extracted-translations.json | 7 + .../components/group-members-list.tsx | 82 ++++++ .../components/group-members.tsx | 93 +++---- .../managed-user-dropdown-button.tsx | 40 +++ .../managed-users/managed-user-row.tsx | 107 ++++++++ .../managed-users/managed-user-status.tsx | 56 ++++ .../managed-users/managed-users-list.tsx | 82 ++++++ .../frontend/stylesheets/_style_includes.less | 1 + .../stylesheets/components/group-members.less | 37 +++ services/web/locales/en.json | 6 + .../components/group-members.spec.tsx | 2 +- .../managed-group-members.spec.tsx | 224 ++++++++++++++++ .../managed-user-dropdown-button.spec.tsx | 63 +++++ .../managed-users/managed-user-row.spec.tsx | 246 ++++++++++++++++++ .../managed-user-status.spec.tsx | 86 ++++++ .../managed-users/managed-users-list.spec.tsx | 97 +++++++ .../UserMembershipControllerTests.js | 21 ++ .../UserMembershipViewModelTests.js | 8 + services/web/types/group-management/user.ts | 7 + 24 files changed, 1237 insertions(+), 63 deletions(-) create mode 100644 services/web/frontend/js/features/group-management/components/group-members-list.tsx create mode 100644 services/web/frontend/js/features/group-management/components/managed-users/managed-user-dropdown-button.tsx create mode 100644 services/web/frontend/js/features/group-management/components/managed-users/managed-user-row.tsx create mode 100644 services/web/frontend/js/features/group-management/components/managed-users/managed-user-status.tsx create mode 100644 services/web/frontend/js/features/group-management/components/managed-users/managed-users-list.tsx create mode 100644 services/web/frontend/stylesheets/components/group-members.less create mode 100644 services/web/test/frontend/features/group-management/components/managed-users/managed-group-members.spec.tsx create mode 100644 services/web/test/frontend/features/group-management/components/managed-users/managed-user-dropdown-button.spec.tsx create mode 100644 services/web/test/frontend/features/group-management/components/managed-users/managed-user-row.spec.tsx create mode 100644 services/web/test/frontend/features/group-management/components/managed-users/managed-user-status.spec.tsx create mode 100644 services/web/test/frontend/features/group-management/components/managed-users/managed-users-list.spec.tsx diff --git a/services/web/app/src/Features/UserMembership/UserMembershipController.js b/services/web/app/src/Features/UserMembership/UserMembershipController.js index 7d1d45f1d6..a75ae2c371 100644 --- a/services/web/app/src/Features/UserMembership/UserMembershipController.js +++ b/services/web/app/src/Features/UserMembership/UserMembershipController.js @@ -17,9 +17,15 @@ const EmailHelper = require('../Helpers/EmailHelper') const { csvAttachment } = require('../../infrastructure/Response') const { UserIsManagerError } = require('./UserMembershipErrors') const CSVParser = require('json2csv').Parser +const Settings = require('@overleaf/settings') + +function isManagedUsersActiveOnGroup(entity) { + return !!(Settings.managedUsers?.enabled && entity.groupPolicy) +} async function manageGroupMembers(req, res, next) { const { entity, entityConfig } = req + const managedUsersActive = isManagedUsersActiveOnGroup(entity) return entity.fetchV1Data(function (error, entity) { if (error != null) { return next(error) @@ -42,6 +48,7 @@ async function manageGroupMembers(req, res, next) { groupId: entityPrimaryKey, users, groupSize: entity.membersLimit, + managedUsersActive, }) } ) diff --git a/services/web/app/src/Features/UserMembership/UserMembershipHandler.js b/services/web/app/src/Features/UserMembership/UserMembershipHandler.js index 14070029e0..a9665632d0 100644 --- a/services/web/app/src/Features/UserMembership/UserMembershipHandler.js +++ b/services/web/app/src/Features/UserMembership/UserMembershipHandler.js @@ -106,7 +106,21 @@ function getPopulatedListOfMembers(entity, attributes, callback) { } } - async.map(userObjects, UserMembershipViewModel.buildAsync, callback) + async.map(userObjects, UserMembershipViewModel.buildAsync, (err, users) => { + if (err) { + return callback(err) + } + for (const user of users) { + if ( + user?._id && + entity?.admin_id && + user._id.toString() === entity.admin_id.toString() + ) { + user.isEntityAdmin = true + } + } + callback(null, users) + }) } function addUserToEntity(entity, attribute, user, callback) { diff --git a/services/web/app/src/Features/UserMembership/UserMembershipViewModel.js b/services/web/app/src/Features/UserMembership/UserMembershipViewModel.js index 3b43578797..550f666312 100644 --- a/services/web/app/src/Features/UserMembership/UserMembershipViewModel.js +++ b/services/web/app/src/Features/UserMembership/UserMembershipViewModel.js @@ -39,6 +39,7 @@ module.exports = UserMembershipViewModel = { last_name: 1, lastLoggedIn: 1, lastActive: 1, + enrollment: 1, } return UserGetter.getUser(userId, projection, function (error, user) { if (error != null || user == null) { @@ -61,6 +62,12 @@ function buildUserViewModel(user, isInvite) { last_active_at: user.lastActive || user.lastLoggedIn || null, last_logged_in_at: user.lastLoggedIn || null, invite: isInvite, + enrollment: user.enrollment + ? { + managedBy: user.enrollment.managedBy, + enrolledAt: user.enrollment.enrolledAt, + } + : undefined, } } 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 264b8a7222..5760c0b432 100644 --- a/services/web/app/views/user_membership/group-members-react.pug +++ b/services/web/app/views/user_membership/group-members-react.pug @@ -8,6 +8,7 @@ block append meta meta(name="ol-groupId", data-type="string", content=groupId) meta(name="ol-groupName", data-type="string", content=name) meta(name="ol-groupSize", data-type="json", content=groupSize) + meta(name="ol-managedUsersActive", data-type="boolean", content=managedUsersActive) block content main.content.content-alt#subscription-manage-group-root diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index ed8ab31729..37dfa0bedf 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -826,6 +826,10 @@ module.exports = { // ID of the IEEE brand in the rails app ieeeBrandId: intFromEnv('IEEE_BRAND_ID', 15), + + managedUsers: { + enabled: false, + }, } module.exports.mergeWith = function (overrides) { diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index b1f04b0a01..560fbb5dd7 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -198,6 +198,7 @@ "delete_projects": "", "delete_tag": "", "delete_token": "", + "delete_user": "", "delete_your_account": "", "deleted_at": "", "deleted_by_on": "", @@ -395,6 +396,7 @@ "go_to_code_location_in_pdf": "", "go_to_pdf_location_in_code": "", "go_to_settings": "", + "group_admin": "", "group_plan_tooltip": "", "group_plan_with_name_tooltip": "", "group_subscription": "", @@ -577,6 +579,7 @@ "manage_publisher_managers": "", "manage_sessions": "", "manage_subscription": "", + "managed": "", "managed_users": "", "managed_users_explanation": "", "managers_management": "", @@ -624,6 +627,7 @@ "new_to_latex_look_at": "", "newsletter": "", "next_payment_of_x_collectected_on_y": "", + "no_actions": "", "no_comments": "", "no_existing_password": "", "no_folder": "", @@ -646,6 +650,7 @@ "normal": "", "normally_x_price_per_month": "", "normally_x_price_per_year": "", + "not_managed": "", "notification_project_invite_accepted_message": "", "notification_project_invite_message": "", "number_of_users": "", @@ -689,6 +694,7 @@ "pdf_viewer": "", "pdf_viewer_error": "", "pending_additional_licenses": "", + "pending_invite": "", "percent_discount_for_groups": "", "plan": "", "plan_tooltip": "", @@ -848,6 +854,7 @@ "search_search_for": "", "search_whole_word": "", "search_within_selection": "", + "security": "", "select_a_file": "", "select_a_file_figure_modal": "", "select_a_payment_method": "", diff --git a/services/web/frontend/js/features/group-management/components/group-members-list.tsx b/services/web/frontend/js/features/group-management/components/group-members-list.tsx new file mode 100644 index 0000000000..55cf6b2e1a --- /dev/null +++ b/services/web/frontend/js/features/group-management/components/group-members-list.tsx @@ -0,0 +1,82 @@ +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 GroupMemberRow from './group-member-row' + +type GroupMembersListProps = { + handleSelectAllClick: (e: any) => void + selectedUsers: User[] + users: User[] + selectUser: (user: User) => void + unselectUser: (user: User) => void +} + +export default function GroupMembersList({ + handleSelectAllClick, + selectedUsers, + users, + selectUser, + unselectUser, +}: GroupMembersListProps) { + const { t } = useTranslation() + return ( + + ) +} 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 959bba288e..1c86373bba 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 @@ -8,13 +8,13 @@ import { postJSON, } from '../../../infrastructure/fetch-json' import MaterialIcon from '../../../shared/components/material-icon' -import Tooltip from '../../../shared/components/tooltip' import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n' import getMeta from '../../../utils/meta' import { parseEmails } from '../utils/emails' import ErrorAlert, { APIError } from './error-alert' -import GroupMemberRow from './group-member-row' import useUserSelection from '../hooks/use-user-selection' +import ManagedUsersList from './managed-users/managed-users-list' +import GroupMembersList from './group-members-list' export default function GroupMembers() { const { isReady } = useWaitForI18n() @@ -39,6 +39,7 @@ 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 paths = useMemo( () => ({ @@ -90,6 +91,9 @@ export default function GroupMembers() { setRemoveMemberError(undefined) ;(async () => { for (const user of selectedUsers) { + if (user?.enrollment?.managedBy) { + continue + } let url if (paths.removeInvite && user.invite && user._id == null) { url = `${paths.removeInvite}/${encodeURIComponent(user.email)}` @@ -142,6 +146,13 @@ export default function GroupMembers() { return null } + const shouldShowRemoveUsersButton = () => { + return ( + selectedUsers.length > 0 && + !selectedUsers.find((u: User) => !!u?.enrollment?.managedBy) + ) + } + return (
@@ -173,7 +184,7 @@ export default function GroupMembers() { ) : ( <> - {selectedUsers.length > 0 && ( + {shouldShowRemoveUsersButton() && ( @@ -185,67 +196,27 @@ export default function GroupMembers() {
-
    -
  • - - - - - {t('email')} - - - {t('name')} - - - - - {t('last_active')} - (?) - - - - - {t('accepted_invite')} - - -
  • - {users.length === 0 && ( -
  • - - - {t('no_members')} - - -
  • - )} - {users.map((user: any) => ( - - ))} -
+ {managedUsersActive ? ( + + ) : ( + + )}

{users.length < groupSize && ( -
+

{t('add_more_members')}

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 new file mode 100644 index 0000000000..c91642f921 --- /dev/null +++ b/services/web/frontend/js/features/group-management/components/managed-users/managed-user-dropdown-button.tsx @@ -0,0 +1,40 @@ +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' + +type ManagedUserDropdownButtonProps = { + user: User +} + +export default function ManagedUserDropdownButton({ + user, +}: ManagedUserDropdownButtonProps) { + const { t } = useTranslation() + return ( + + +