From b7108f7874b6f5ef9501950d4d4913fc163e9fb1 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Tue, 31 Jan 2023 09:42:52 -0600 Subject: [PATCH] Merge pull request #11515 from overleaf/ab-group-membership-dash-react [web] Managed groups in React subscription dash GitOrigin-RevId: 4811d8dd2b42fa9ad83b5c4f12582e7bc04bad40 --- .../SubscriptionViewModelBuilder.js | 33 +++-- .../views/subscriptions/dashboard-react.pug | 1 + .../web/frontend/extracted-translations.json | 5 + .../dashboard/managed-group-subscriptions.tsx | 74 ++++++++++ .../dashboard/personal-subscription.tsx | 1 + .../dashboard/subscription-dashboard.tsx | 11 +- services/web/locales/en.json | 3 + .../managed-group-subscriptions.test.tsx | 94 ++++++++++++ .../subscription/fixtures/subscriptions.tsx | 138 +++++++++--------- .../subscription/dashboard/subscription.ts | 4 + 10 files changed, 287 insertions(+), 77 deletions(-) create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/managed-group-subscriptions.tsx create mode 100644 services/web/test/frontend/features/subscription/components/dashboard/managed-group-subscriptions.test.tsx diff --git a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js index 7830517f81..a455bd630f 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js +++ b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js @@ -25,6 +25,13 @@ function buildHostedLink(type) { return `/user/subscription/recurly/${type}` } +// Downgrade from Mongoose object, so we can add custom attributes to object +function serializeMongooseObject(object) { + return object && typeof object.toObject === 'function' + ? object.toObject() + : object +} + async function getRedirectToHostedPage(userId, pageType) { if (!['billing-details', 'account-management'].includes(pageType)) { throw new InvalidError('unexpected page type') @@ -178,13 +185,13 @@ function buildUsersSubscriptionViewModel(user, callback) { recurlyCoupons = [] } - if ( - personalSubscription && - typeof personalSubscription.toObject === 'function' - ) { - // Downgrade from Mongoose object, so we can add a recurly and plan attribute - personalSubscription = personalSubscription.toObject() - } + personalSubscription = serializeMongooseObject(personalSubscription) + memberGroupSubscriptions = memberGroupSubscriptions.map( + serializeMongooseObject + ) + managedGroupSubscriptions = managedGroupSubscriptions.map( + serializeMongooseObject + ) if (plan != null) { personalSubscription.plan = plan @@ -306,7 +313,11 @@ function buildUsersSubscriptionViewModel(user, callback) { } for (const memberGroupSubscription of memberGroupSubscriptions) { - if (memberGroupSubscription.manager_ids?.includes(user._id)) { + if ( + memberGroupSubscription.manager_ids?.some( + id => id.toString() === user._id.toString() + ) + ) { memberGroupSubscription.userIsGroupManager = true } if (memberGroupSubscription.teamNotice) { @@ -318,7 +329,11 @@ function buildUsersSubscriptionViewModel(user, callback) { } for (const managedGroupSubscription of managedGroupSubscriptions) { - if (managedGroupSubscription.member_ids?.includes(user._id)) { + if ( + managedGroupSubscription.member_ids?.some( + id => id.toString() === user._id.toString() + ) + ) { managedGroupSubscription.userIsGroupMember = true } buildGroupSubscriptionForView(managedGroupSubscription) diff --git a/services/web/app/views/subscriptions/dashboard-react.pug b/services/web/app/views/subscriptions/dashboard-react.pug index 932b5df240..1249864b8c 100644 --- a/services/web/app/views/subscriptions/dashboard-react.pug +++ b/services/web/app/views/subscriptions/dashboard-react.pug @@ -8,6 +8,7 @@ block head-scripts block append meta meta(name="ol-managedInstitutions", data-type="json", content=managedInstitutions) + meta(name="ol-managedGroupSubscriptions", data-type="json" content=managedGroupSubscriptions) meta(name="ol-planCodesChangingAtTermEnd", data-type="json", content=plans.planCodesChangingAtTermEnd) meta(name="ol-currentInstitutionsWithLicence", data-type="json" content=currentInstitutionsWithLicence) if (personalSubscription && personalSubscription.recurly) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 25e4fbc628..1290f56c13 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -406,7 +406,9 @@ "make_private": "", "manage_beta_program_membership": "", "manage_files_from_your_dropbox_folder": "", + "manage_group_managers": "", "manage_labs_program_membership": "", + "manage_members": "", "manage_newsletter": "", "manage_sessions": "", "math_display": "", @@ -763,6 +765,7 @@ "validation_issue_entry_description": "", "view_all": "", "view_logs": "", + "view_metrics": "", "view_pdf": "", "view_your_invoices": "", "want_change_to_apply_before_plan_end": "", @@ -775,6 +778,8 @@ "word_count": "", "work_offline": "", "work_with_non_overleaf_users": "", + "you_are_a_manager_and_member_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "", + "you_are_a_manager_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "", "you_are_on_x_plan_as_a_confirmed_member_of_institution_y": "", "you_can_now_log_in_sso": "", "you_dont_have_any_repositories": "", diff --git a/services/web/frontend/js/features/subscription/components/dashboard/managed-group-subscriptions.tsx b/services/web/frontend/js/features/subscription/components/dashboard/managed-group-subscriptions.tsx new file mode 100644 index 0000000000..cecccb53de --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/managed-group-subscriptions.tsx @@ -0,0 +1,74 @@ +import { Trans, useTranslation } from 'react-i18next' +import { GroupSubscription } from '../../../../../../types/subscription/dashboard/subscription' +import { User } from '../../../../../../types/user' + +export type ManagedGroupSubscription = Omit & { + userIsGroupMember: boolean + planLevelName: string + admin_id: User +} + +type ManagedGroupSubscriptionsProps = { + subscriptions?: ManagedGroupSubscription[] +} + +export default function ManagedGroupSubscriptions({ + subscriptions, +}: ManagedGroupSubscriptionsProps) { + const { t } = useTranslation() + + if (!subscriptions) { + return null + } + + return ( + <> + {subscriptions.map(subscription => ( +
+

+ {subscription.userIsGroupMember ? ( + , ]} // eslint-disable-line react/jsx-key, jsx-a11y/anchor-has-content + values={{ + planName: subscription.planLevelName, + groupName: subscription.teamName || '', + adminEmail: subscription.admin_id.email, + }} + /> + ) : ( + , ]} // eslint-disable-line react/jsx-key, jsx-a11y/anchor-has-content + values={{ + planName: subscription.planLevelName, + groupName: subscription.teamName || '', + adminEmail: subscription.admin_id.email, + }} + /> + )} +

+

+ + {t('manage_members')} + +

+

+ + {t('manage_group_managers')} + +

+

+ + {t('view_metrics')} + +

+
+
+ ))} + + ) +} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx b/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx index fbcea64113..49e5a27ac8 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx @@ -77,6 +77,7 @@ function PersonalSubscription({ {t('payment_provider_unreachable_error')} )} +
) } diff --git a/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx b/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx index 4f7818303e..9a1d487407 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx @@ -3,14 +3,18 @@ import getMeta from '../../../../utils/meta' import InstitutionMemberships from './institution-memberships' import FreePlan from './free-plan' import PersonalSubscription from './personal-subscription' +import ManagedGroupSubscriptions from './managed-group-subscriptions' function SubscriptionDashboard() { const { t } = useTranslation() const institutionMemberships = getMeta('ol-currentInstitutionsWithLicence') const subscription = getMeta('ol-subscription') + const managedGroupSubscriptions = getMeta('ol-managedGroupSubscriptions') const hasDisplayedSubscription = - institutionMemberships?.length > 0 || subscription + institutionMemberships?.length > 0 || + subscription || + managedGroupSubscriptions?.length > 0 return (
@@ -21,8 +25,11 @@ function SubscriptionDashboard() {

{t('your_subscription')}

- + + {!hasDisplayedSubscription && } diff --git a/services/web/locales/en.json b/services/web/locales/en.json index a7610b9a0f..55bc9b55a2 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -869,7 +869,9 @@ "make_private": "Make Private", "manage_beta_program_membership": "Manage Beta Program Membership", "manage_files_from_your_dropbox_folder": "Manage files from your Dropbox folder", + "manage_group_managers": "Manage group managers", "manage_labs_program_membership": "Manage Labs Program Membership", + "manage_members": "Manage members", "manage_newsletter": "Manage Your Newsletter Preferences", "manage_sessions": "Manage Your Sessions", "manage_subscription": "Manage Subscription", @@ -1587,6 +1589,7 @@ "view_collab_edits": "View collaborator edits ", "view_in_template_gallery": "View it in the template gallery", "view_logs": "View logs", + "view_metrics": "View metrics", "view_other_options_to_log_in": "View other options to log in", "view_pdf": "View PDF", "view_single_version": "View single version", diff --git a/services/web/test/frontend/features/subscription/components/dashboard/managed-group-subscriptions.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/managed-group-subscriptions.test.tsx new file mode 100644 index 0000000000..6a0d72c4be --- /dev/null +++ b/services/web/test/frontend/features/subscription/components/dashboard/managed-group-subscriptions.test.tsx @@ -0,0 +1,94 @@ +import { expect } from 'chai' +import { render, screen } from '@testing-library/react' +import ManagedGroupSubscriptions, { + ManagedGroupSubscription, +} from '../../../../../../frontend/js/features/subscription/components/dashboard/managed-group-subscriptions' +import { + groupActiveSubscription, + groupActiveSubscriptionWithPendingLicenseChange, +} from '../../fixtures/subscriptions' + +const managedGroupSubscriptions: ManagedGroupSubscription[] = [ + { + ...groupActiveSubscription, + userIsGroupMember: true, + planLevelName: 'Professional', + admin_id: { + id: 'abc123abc123', + email: 'you@example.com', + }, + }, + { + ...groupActiveSubscriptionWithPendingLicenseChange, + userIsGroupMember: false, + planLevelName: 'Collaborator', + admin_id: { + id: 'bcd456bcd456', + email: 'someone@example.com', + }, + }, +] + +describe('', function () { + beforeEach(function () { + window.metaAttributesCache = new Map() + }) + + afterEach(function () { + window.metaAttributesCache = new Map() + }) + + it('renders all managed group subscriptions', function () { + render( + + ) + + const elements = screen.getAllByText('You are a', { + exact: false, + }) + expect(elements.length).to.equal(2) + expect(elements[0].textContent).to.equal( + 'You are a manager and member of the Professional group subscription GAS administered by you@example.com' + ) + expect(elements[1].textContent).to.equal( + 'You are a manager of the Collaborator group subscription GASWPLC administered by someone@example.com' + ) + + const manageMembersLinks = screen.getAllByText('Manage members') + expect(manageMembersLinks.length).to.equal(2) + expect(manageMembersLinks[0].getAttribute('href')).to.equal( + '/manage/groups/bcd567/members' + ) + expect(manageMembersLinks[1].getAttribute('href')).to.equal( + '/manage/groups/def456/members' + ) + + const manageGroupManagersLinks = screen.getAllByText( + 'Manage group managers' + ) + expect(manageGroupManagersLinks.length).to.equal(2) + expect(manageGroupManagersLinks[0].getAttribute('href')).to.equal( + '/manage/groups/bcd567/managers' + ) + expect(manageGroupManagersLinks[1].getAttribute('href')).to.equal( + '/manage/groups/def456/managers' + ) + + const viewMetricsLinks = screen.getAllByText('View metrics') + expect(viewMetricsLinks.length).to.equal(2) + expect(viewMetricsLinks[0].getAttribute('href')).to.equal( + '/metrics/groups/bcd567' + ) + expect(viewMetricsLinks[1].getAttribute('href')).to.equal( + '/metrics/groups/def456' + ) + }) + + it('renders nothing when there are no group memberships', function () { + render() + const elements = screen.queryAllByText('You are a', { + exact: false, + }) + expect(elements.length).to.equal(0) + }) +}) diff --git a/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx b/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx index 353e4af699..38953bd27e 100644 --- a/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx +++ b/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx @@ -1,4 +1,7 @@ -import { Subscription } from '../../../../../types/subscription/dashboard/subscription' +import { + GroupSubscription, + Subscription, +} from '../../../../../types/subscription/dashboard/subscription' const dateformat = require('dateformat') const today = new Date() const oneYearFromToday = new Date().setFullYear(today.getFullYear() + 1) @@ -177,13 +180,14 @@ export const pendingSubscriptionChange: Subscription = { }, } -export const groupActiveSubscription: Subscription = { +export const groupActiveSubscription: GroupSubscription = { manager_ids: ['abc123'], member_ids: ['abc123'], invited_emails: [], groupPlan: true, + teamName: 'GAS', membersLimit: 10, - _id: 'def456', + _id: 'bcd567', admin_id: 'abc123', teamInvites: [], planCode: 'group_collaborator_10_enterprise', @@ -220,72 +224,74 @@ export const groupActiveSubscription: Subscription = { }, } -export const groupActiveSubscriptionWithPendingLicenseChange: Subscription = { - manager_ids: ['abc123'], - member_ids: ['abc123'], - invited_emails: [], - groupPlan: true, - membersLimit: 10, - _id: 'def456', - admin_id: 'abc123', - teamInvites: [], - planCode: 'group_collaborator_10_enterprise', - recurlySubscription_id: 'ghi789', - plan: { - planCode: 'group_collaborator_10_enterprise', - name: 'Overleaf Standard (Collaborator) - Group Account (10 licenses) - Enterprise', - hideFromUsers: true, - price_in_cents: 129000, - annual: true, - features: {}, +export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription = + { + manager_ids: ['abc123'], + member_ids: ['abc123'], + invited_emails: [], groupPlan: true, + teamName: 'GASWPLC', membersLimit: 10, - membersLimitAddOn: 'additional-license', - }, - recurly: { - tax: 0, - taxRate: 0, - billingDetailsLink: '/user/subscription/recurly/billing-details', - accountManagementLink: '/user/subscription/recurly/account-management', - additionalLicenses: 11, - totalLicenses: 21, - nextPaymentDueAt, - currency: 'USD', - state: 'active', - trialEndsAtFormatted: null, - trial_ends_at: null, - activeCoupons: [], - account: { - has_canceled_subscription: { - _: 'false', - $: { - type: 'boolean', - }, - }, - has_past_due_invoice: { - _: 'false', - $: { - type: 'boolean', - }, - }, + _id: 'def456', + admin_id: 'abc123', + teamInvites: [], + planCode: 'group_collaborator_10_enterprise', + recurlySubscription_id: 'ghi789', + plan: { + planCode: 'group_collaborator_10_enterprise', + name: 'Overleaf Standard (Collaborator) - Group Account (10 licenses) - Enterprise', + hideFromUsers: true, + price_in_cents: 129000, + annual: true, + features: {}, + groupPlan: true, + membersLimit: 10, + membersLimitAddOn: 'additional-license', }, - displayPrice: '$2967.00', - currentPlanDisplayPrice: '$2709.00', - pendingAdditionalLicenses: 13, - pendingTotalLicenses: 23, - }, - pendingPlan: { - planCode: 'group_collaborator_10_enterprise', - name: 'Overleaf Standard (Collaborator) - Group Account (10 licenses) - Enterprise', - hideFromUsers: true, - price_in_cents: 129000, - annual: true, - features: {}, - groupPlan: true, - membersLimit: 10, - membersLimitAddOn: 'additional-license', - }, -} + recurly: { + tax: 0, + taxRate: 0, + billingDetailsLink: '/user/subscription/recurly/billing-details', + accountManagementLink: '/user/subscription/recurly/account-management', + additionalLicenses: 11, + totalLicenses: 21, + nextPaymentDueAt, + currency: 'USD', + state: 'active', + trialEndsAtFormatted: null, + trial_ends_at: null, + activeCoupons: [], + account: { + has_canceled_subscription: { + _: 'false', + $: { + type: 'boolean', + }, + }, + has_past_due_invoice: { + _: 'false', + $: { + type: 'boolean', + }, + }, + }, + displayPrice: '$2967.00', + currentPlanDisplayPrice: '$2709.00', + pendingAdditionalLicenses: 13, + pendingTotalLicenses: 23, + }, + pendingPlan: { + planCode: 'group_collaborator_10_enterprise', + name: 'Overleaf Standard (Collaborator) - Group Account (10 licenses) - Enterprise', + hideFromUsers: true, + price_in_cents: 129000, + annual: true, + features: {}, + groupPlan: true, + membersLimit: 10, + membersLimitAddOn: 'additional-license', + }, + } export const trialSubscription: Subscription = { manager_ids: ['abc123'], diff --git a/services/web/types/subscription/dashboard/subscription.ts b/services/web/types/subscription/dashboard/subscription.ts index 8d2e5d4810..3add94fe34 100644 --- a/services/web/types/subscription/dashboard/subscription.ts +++ b/services/web/types/subscription/dashboard/subscription.ts @@ -62,3 +62,7 @@ export type Subscription = { } pendingPlan?: Plan } + +export type GroupSubscription = Subscription & { + teamName: string +}