Browse Source

Merge pull request #21782 from overleaf/ls-update-group-member-management-page

update group member management page for flexible licensing

GitOrigin-RevId: 605fb760a1f73763e49978cf4aea81bb88ffb425
Liangjun Song 1 year ago
parent
commit
56f6f77ba2

+ 7 - 0
services/web/app/src/Features/UserMembership/UserMembershipController.mjs

@@ -11,6 +11,7 @@ import {
 import { SSOConfig } from '../../models/SSOConfig.js'
 import { SSOConfig } from '../../models/SSOConfig.js'
 import { Parser as CSVParser } from 'json2csv'
 import { Parser as CSVParser } from 'json2csv'
 import { expressify } from '@overleaf/promise-utils'
 import { expressify } from '@overleaf/promise-utils'
+import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
 
 
 async function manageGroupMembers(req, res, next) {
 async function manageGroupMembers(req, res, next) {
   const { entity: subscription, entityConfig } = req
   const { entity: subscription, entityConfig } = req
@@ -29,6 +30,12 @@ async function manageGroupMembers(req, res, next) {
   )
   )
   const ssoConfig = await SSOConfig.findById(subscription.ssoConfig).exec()
   const ssoConfig = await SSOConfig.findById(subscription.ssoConfig).exec()
 
 
+  await SplitTestHandler.promises.getAssignment(
+    req,
+    res,
+    'flexible-group-licensing'
+  )
+
   res.render('user_membership/group-members-react', {
   res.render('user_membership/group-members-react', {
     name: entityName,
     name: entityName,
     groupId: entityPrimaryKey,
     groupId: entityPrimaryKey,

+ 5 - 0
services/web/frontend/extracted-translations.json

@@ -73,6 +73,7 @@
   "add_more_editors": "",
   "add_more_editors": "",
   "add_more_managers": "",
   "add_more_managers": "",
   "add_more_members": "",
   "add_more_members": "",
+  "add_more_users": "",
   "add_new_email": "",
   "add_new_email": "",
   "add_ons_are": "",
   "add_ons_are": "",
   "add_or_remove_project_from_tag": "",
   "add_or_remove_project_from_tag": "",
@@ -751,11 +752,13 @@
   "invite": "",
   "invite": "",
   "invite_expired": "",
   "invite_expired": "",
   "invite_more_collabs": "",
   "invite_more_collabs": "",
+  "invite_more_members": "",
   "invite_not_accepted": "",
   "invite_not_accepted": "",
   "invite_resend_limit_hit": "",
   "invite_resend_limit_hit": "",
   "invited_to_group": "",
   "invited_to_group": "",
   "invited_to_group_have_individual_subcription": "",
   "invited_to_group_have_individual_subcription": "",
   "invited_to_join": "",
   "invited_to_join": "",
+  "inviting": "",
   "ip_address": "",
   "ip_address": "",
   "is_email_affiliated": "",
   "is_email_affiliated": "",
   "issued_on": "",
   "issued_on": "",
@@ -1871,10 +1874,12 @@
   "you_dont_have_any_repositories": "",
   "you_dont_have_any_repositories": "",
   "you_have_0_free_suggestions_left": "",
   "you_have_0_free_suggestions_left": "",
   "you_have_1_free_suggestion_left": "",
   "you_have_1_free_suggestion_left": "",
+  "you_have_1_user_and_your_plan_supports_up_to_y": "",
   "you_have_added_x_of_group_size_y": "",
   "you_have_added_x_of_group_size_y": "",
   "you_have_been_invited_to_transfer_management_of_your_account": "",
   "you_have_been_invited_to_transfer_management_of_your_account": "",
   "you_have_been_invited_to_transfer_management_of_your_account_to": "",
   "you_have_been_invited_to_transfer_management_of_your_account_to": "",
   "you_have_been_removed_from_this_project_and_will_be_redirected_to_project_dashboard": "",
   "you_have_been_removed_from_this_project_and_will_be_redirected_to_project_dashboard": "",
+  "you_have_x_users_and_your_plan_supports_up_to_y": "",
   "you_need_to_configure_your_sso_settings": "",
   "you_need_to_configure_your_sso_settings": "",
   "you_will_be_able_to_reassign_subscription": "",
   "you_will_be_able_to_reassign_subscription": "",
   "youll_get_best_results_in_visual_but_can_be_used_in_source": "",
   "youll_get_best_results_in_visual_but_can_be_used_in_source": "",

+ 58 - 15
services/web/frontend/js/features/group-management/components/group-members.tsx

@@ -7,6 +7,7 @@ import getMeta from '../../../utils/meta'
 import { useGroupMembersContext } from '../context/group-members-context'
 import { useGroupMembersContext } from '../context/group-members-context'
 import ErrorAlert from './error-alert'
 import ErrorAlert from './error-alert'
 import MembersList from './members-table/members-list'
 import MembersList from './members-table/members-list'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
 
 
 export default function GroupMembers() {
 export default function GroupMembers() {
   const { isReady } = useWaitForI18n()
   const { isReady } = useWaitForI18n()
@@ -23,6 +24,9 @@ export default function GroupMembers() {
     paths,
     paths,
   } = useGroupMembersContext()
   } = useGroupMembersContext()
   const [emailString, setEmailString] = useState<string>('')
   const [emailString, setEmailString] = useState<string>('')
+  const flexibleGroupLicensingEnabled = useFeatureFlag(
+    'flexible-group-licensing'
+  )
 
 
   const groupId = getMeta('ol-groupId')
   const groupId = getMeta('ol-groupId')
   const groupName = getMeta('ol-groupName')
   const groupName = getMeta('ol-groupName')
@@ -44,6 +48,43 @@ export default function GroupMembers() {
     addMembers(emailString)
     addMembers(emailString)
   }
   }
 
 
+  const groupSizeDetails = () => {
+    if (flexibleGroupLicensingEnabled) {
+      return (
+        <small data-testid="group-size-details">
+          <strong>
+            {users.length === 1
+              ? t('you_have_1_user_and_your_plan_supports_up_to_y', {
+                  groupSize,
+                })
+              : t('you_have_x_users_and_your_plan_supports_up_to_y', {
+                  addedUsersSize: users.length,
+                  groupSize,
+                })}
+          </strong>{' '}
+          <a
+            href="/user/subscription/group/add-users"
+            rel="noreferrer noopener"
+          >
+            {t('add_more_users')}
+          </a>
+        </small>
+      )
+    }
+
+    return (
+      <small>
+        <Trans
+          i18nKey="you_have_added_x_of_group_size_y"
+          components={[<strong />, <strong />]} // eslint-disable-line react/jsx-key
+          values={{ addedUsersSize: users.length, groupSize }}
+          shouldUnescape
+          tOptions={{ interpolation: { escapeValue: true } }}
+        />
+      </small>
+    )
+  }
+
   return (
   return (
     <div className="container">
     <div className="container">
       <Row>
       <Row>
@@ -60,17 +101,7 @@ export default function GroupMembers() {
           <div className="card">
           <div className="card">
             <div className="page-header">
             <div className="page-header">
               <div className="pull-right">
               <div className="pull-right">
-                {selectedUsers.length === 0 && (
-                  <small>
-                    <Trans
-                      i18nKey="you_have_added_x_of_group_size_y"
-                      components={[<strong />, <strong />]} // eslint-disable-line react/jsx-key
-                      values={{ addedUsersSize: users.length, groupSize }}
-                      shouldUnescape
-                      tOptions={{ interpolation: { escapeValue: true } }}
-                    />
-                  </small>
-                )}
+                {selectedUsers.length === 0 && groupSizeDetails()}
                 {removeMemberLoading ? (
                 {removeMemberLoading ? (
                   <Button bsStyle="danger" disabled>
                   <Button bsStyle="danger" disabled>
                     {t('removing')}&hellip;
                     {t('removing')}&hellip;
@@ -93,8 +124,15 @@ export default function GroupMembers() {
             </div>
             </div>
             <hr />
             <hr />
             {users.length < groupSize && (
             {users.length < groupSize && (
-              <div className="add-more-members-form">
-                <p className="small">{t('add_more_members')}</p>
+              <div
+                className="add-more-members-form"
+                data-testid="add-more-members-form"
+              >
+                <p className="small">
+                  {flexibleGroupLicensingEnabled
+                    ? t('invite_more_members')
+                    : t('add_more_members')}
+                </p>
                 <ErrorAlert error={inviteError} />
                 <ErrorAlert error={inviteError} />
                 <Form horizontal onSubmit={onAddMembersSubmit} className="form">
                 <Form horizontal onSubmit={onAddMembersSubmit} className="form">
                   <Row>
                   <Row>
@@ -110,11 +148,16 @@ export default function GroupMembers() {
                     <Col xs={4}>
                     <Col xs={4}>
                       {inviteMemberLoading ? (
                       {inviteMemberLoading ? (
                         <Button bsStyle="primary" disabled>
                         <Button bsStyle="primary" disabled>
-                          {t('adding')}&hellip;
+                          {flexibleGroupLicensingEnabled
+                            ? t('inviting')
+                            : t('adding')}
+                          &hellip;
                         </Button>
                         </Button>
                       ) : (
                       ) : (
                         <Button bsStyle="primary" onClick={onAddMembersSubmit}>
                         <Button bsStyle="primary" onClick={onAddMembersSubmit}>
-                          {t('add')}
+                          {flexibleGroupLicensingEnabled
+                            ? t('invite')
+                            : t('add')}
                         </Button>
                         </Button>
                       )}
                       )}
                     </Col>
                     </Col>

+ 6 - 3
services/web/frontend/js/pages/user/subscription/group-management/group-members.jsx

@@ -2,13 +2,16 @@ import '../base'
 import ReactDOM from 'react-dom'
 import ReactDOM from 'react-dom'
 import GroupMembers from '../../../../features/group-management/components/group-members'
 import GroupMembers from '../../../../features/group-management/components/group-members'
 import { GroupMembersProvider } from '../../../../features/group-management/context/group-members-context'
 import { GroupMembersProvider } from '../../../../features/group-management/context/group-members-context'
+import { SplitTestProvider } from '@/shared/context/split-test-context'
 
 
 const element = document.getElementById('subscription-manage-group-root')
 const element = document.getElementById('subscription-manage-group-root')
 if (element) {
 if (element) {
   ReactDOM.render(
   ReactDOM.render(
-    <GroupMembersProvider>
-      <GroupMembers />
-    </GroupMembersProvider>,
+    <SplitTestProvider>
+      <GroupMembersProvider>
+        <GroupMembers />
+      </GroupMembersProvider>
+    </SplitTestProvider>,
     element
     element
   )
   )
 }
 }

+ 5 - 0
services/web/locales/en.json

@@ -85,6 +85,7 @@
   "add_more_editors": "Add more editors",
   "add_more_editors": "Add more editors",
   "add_more_managers": "Add more managers",
   "add_more_managers": "Add more managers",
   "add_more_members": "Add more members",
   "add_more_members": "Add more members",
+  "add_more_users": "Add more users.",
   "add_new_email": "Add new email",
   "add_new_email": "Add new email",
   "add_ons_are": "<strong>Add-ons:</strong> __addOnName__",
   "add_ons_are": "<strong>Add-ons:</strong> __addOnName__",
   "add_or_remove_project_from_tag": "Add or remove project from tag __tagName__",
   "add_or_remove_project_from_tag": "Add or remove project from tag __tagName__",
@@ -1051,6 +1052,7 @@
   "invite": "Invite",
   "invite": "Invite",
   "invite_expired": "The invite may have expired",
   "invite_expired": "The invite may have expired",
   "invite_more_collabs": "Invite more collaborators",
   "invite_more_collabs": "Invite more collaborators",
+  "invite_more_members": "Invite more members",
   "invite_not_accepted": "Invite not yet accepted",
   "invite_not_accepted": "Invite not yet accepted",
   "invite_not_valid": "This is not a valid project invite",
   "invite_not_valid": "This is not a valid project invite",
   "invite_not_valid_description": "The invite may have expired. Please contact the project owner",
   "invite_not_valid_description": "The invite may have expired. Please contact the project owner",
@@ -1062,6 +1064,7 @@
   "invited_to_group_register": "To accept __inviterName__’s invitation you’ll need to create an account.",
   "invited_to_group_register": "To accept __inviterName__’s invitation you’ll need to create an account.",
   "invited_to_group_register_benefits": "__appName__ is a collaborative online LaTeX editor, with thousands of ready-to-use templates and an array of LaTeX learning resources to help you get started.",
   "invited_to_group_register_benefits": "__appName__ is a collaborative online LaTeX editor, with thousands of ready-to-use templates and an array of LaTeX learning resources to help you get started.",
   "invited_to_join": "You have been invited to join",
   "invited_to_join": "You have been invited to join",
+  "inviting": "Inviting",
   "ip_address": "IP Address",
   "ip_address": "IP Address",
   "is_email_affiliated": "Is your email affiliated with an institution? ",
   "is_email_affiliated": "Is your email affiliated with an institution? ",
   "is_longer_than_n_characters": "is at least __n__ characters long",
   "is_longer_than_n_characters": "is at least __n__ characters long",
@@ -2535,10 +2538,12 @@
   "you_get_access_to_info": "These features are available only to you (the subscriber).",
   "you_get_access_to_info": "These features are available only to you (the subscriber).",
   "you_have_0_free_suggestions_left": "You have 0 free suggestions left",
   "you_have_0_free_suggestions_left": "You have 0 free suggestions left",
   "you_have_1_free_suggestion_left": "You have 1 free suggestion left",
   "you_have_1_free_suggestion_left": "You have 1 free suggestion left",
+  "you_have_1_user_and_your_plan_supports_up_to_y": "You have 1 user and your plan supports up to __groupSize__.",
   "you_have_added_x_of_group_size_y": "You have added <0>__addedUsersSize__</0> of <1>__groupSize__</1> available members",
   "you_have_added_x_of_group_size_y": "You have added <0>__addedUsersSize__</0> of <1>__groupSize__</1> available members",
   "you_have_been_invited_to_transfer_management_of_your_account": "You have been invited to transfer management of your account.",
   "you_have_been_invited_to_transfer_management_of_your_account": "You have been invited to transfer management of your account.",
   "you_have_been_invited_to_transfer_management_of_your_account_to": "You have been invited to transfer management of your account to __groupName__.",
   "you_have_been_invited_to_transfer_management_of_your_account_to": "You have been invited to transfer management of your account to __groupName__.",
   "you_have_been_removed_from_this_project_and_will_be_redirected_to_project_dashboard": "You have been removed from this project, and will no longer have access to it. You will be redirected to your project dashboard momentarily.",
   "you_have_been_removed_from_this_project_and_will_be_redirected_to_project_dashboard": "You have been removed from this project, and will no longer have access to it. You will be redirected to your project dashboard momentarily.",
+  "you_have_x_users_and_your_plan_supports_up_to_y": "You have __addedUsersSize__ users and your plan supports up to __groupSize__.",
   "you_need_to_configure_your_sso_settings": "You need to configure and test your SSO settings before enabling SSO",
   "you_need_to_configure_your_sso_settings": "You need to configure and test your SSO settings before enabling SSO",
   "you_plus_1": "You + 1",
   "you_plus_1": "You + 1",
   "you_plus_10": "You + 10",
   "you_plus_10": "You + 10",

+ 82 - 6
services/web/test/frontend/features/group-management/components/group-members.spec.tsx

@@ -2,6 +2,7 @@ import '../../../helpers/bootstrap-3'
 import GroupMembers from '@/features/group-management/components/group-members'
 import GroupMembers from '@/features/group-management/components/group-members'
 import { GroupMembersProvider } from '@/features/group-management/context/group-members-context'
 import { GroupMembersProvider } from '@/features/group-management/context/group-members-context'
 import { User } from '../../../../../types/group-management/user'
 import { User } from '../../../../../types/group-management/user'
+import { SplitTestProvider } from '@/shared/context/split-test-context'
 
 
 const GROUP_ID = '777fff777fff'
 const GROUP_ID = '777fff777fff'
 const PATHS = {
 const PATHS = {
@@ -14,9 +15,11 @@ const PATHS = {
 describe('GroupMembers', function () {
 describe('GroupMembers', function () {
   function mountGroupMembersProvider() {
   function mountGroupMembersProvider() {
     cy.mount(
     cy.mount(
-      <GroupMembersProvider>
-        <GroupMembers />
-      </GroupMembersProvider>
+      <SplitTestProvider>
+        <GroupMembersProvider>
+          <GroupMembers />
+        </GroupMembersProvider>
+      </SplitTestProvider>
     )
     )
   }
   }
 
 
@@ -47,9 +50,11 @@ describe('GroupMembers', function () {
       })
       })
 
 
       cy.mount(
       cy.mount(
-        <GroupMembersProvider>
-          <GroupMembers />
-        </GroupMembersProvider>
+        <SplitTestProvider>
+          <GroupMembersProvider>
+            <GroupMembers />
+          </GroupMembersProvider>
+        </SplitTestProvider>
       )
       )
     })
     })
 
 
@@ -456,4 +461,75 @@ describe('GroupMembers', function () {
       })
       })
     })
     })
   })
   })
+
+  describe('with flexible group licensing 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: false,
+    }
+    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,
+    }
+
+    it('renders the group members page with the new text', 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])
+        win.metaAttributesCache.set('ol-splitTestVariants', {
+          'flexible-group-licensing': 'enabled',
+        })
+      })
+
+      cy.mount(
+        <SplitTestProvider>
+          <GroupMembersProvider>
+            <GroupMembers />
+          </GroupMembersProvider>
+        </SplitTestProvider>
+      )
+
+      cy.findByTestId('group-size-details').contains(
+        'You have 2 users and your plan supports up to 10. Add more users.'
+      )
+      cy.findByTestId('add-more-members-form').within(() => {
+        cy.contains('Invite more members')
+        cy.get('button').contains('Invite')
+      })
+    })
+
+    it('renders the group members page with new text when only has one group member', 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])
+        win.metaAttributesCache.set('ol-splitTestVariants', {
+          'flexible-group-licensing': 'enabled',
+        })
+      })
+
+      cy.mount(
+        <SplitTestProvider>
+          <GroupMembersProvider>
+            <GroupMembers />
+          </GroupMembersProvider>
+        </SplitTestProvider>
+      )
+
+      cy.findByTestId('group-size-details').contains(
+        'You have 1 user and your plan supports up to 10. Add more users.'
+      )
+    })
+  })
 })
 })

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

@@ -2,6 +2,7 @@ import '../../../helpers/bootstrap-3'
 import GroupMembers from '@/features/group-management/components/group-members'
 import GroupMembers from '@/features/group-management/components/group-members'
 import { GroupMembersProvider } from '@/features/group-management/context/group-members-context'
 import { GroupMembersProvider } from '@/features/group-management/context/group-members-context'
 import { User } from '../../../../../types/group-management/user'
 import { User } from '../../../../../types/group-management/user'
+import { SplitTestProvider } from '@/shared/context/split-test-context'
 
 
 const GROUP_ID = '777fff777fff'
 const GROUP_ID = '777fff777fff'
 const JOHN_DOE: User = {
 const JOHN_DOE: User = {
@@ -57,9 +58,11 @@ const PATHS = {
 
 
 function mountGroupMembersProvider() {
 function mountGroupMembersProvider() {
   cy.mount(
   cy.mount(
-    <GroupMembersProvider>
-      <GroupMembers />
-    </GroupMembersProvider>
+    <SplitTestProvider>
+      <GroupMembersProvider>
+        <GroupMembers />
+      </GroupMembersProvider>
+    </SplitTestProvider>
   )
   )
 }
 }