Quellcode durchsuchen

Merge pull request #11515 from overleaf/ab-group-membership-dash-react

[web] Managed groups in React subscription dash

GitOrigin-RevId: 4811d8dd2b42fa9ad83b5c4f12582e7bc04bad40
Jessica Lawshe vor 3 Jahren
Ursprung
Commit
b7108f7874

+ 24 - 9
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)

+ 1 - 0
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)

+ 5 - 0
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": "",

+ 74 - 0
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<GroupSubscription, 'admin_id'> & {
+  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 => (
+        <div key={`managed-group-${subscription._id}`}>
+          <p>
+            {subscription.userIsGroupMember ? (
+              <Trans
+                i18nKey="you_are_a_manager_and_member_of_x_plan_as_member_of_group_subscription_y_administered_by_z"
+                components={[<a href="/user/subscription/plans" />, <strong />]} // eslint-disable-line react/jsx-key, jsx-a11y/anchor-has-content
+                values={{
+                  planName: subscription.planLevelName,
+                  groupName: subscription.teamName || '',
+                  adminEmail: subscription.admin_id.email,
+                }}
+              />
+            ) : (
+              <Trans
+                i18nKey="you_are_a_manager_of_x_plan_as_member_of_group_subscription_y_administered_by_z"
+                components={[<a href="/user/subscription/plans" />, <strong />]} // eslint-disable-line react/jsx-key, jsx-a11y/anchor-has-content
+                values={{
+                  planName: subscription.planLevelName,
+                  groupName: subscription.teamName || '',
+                  adminEmail: subscription.admin_id.email,
+                }}
+              />
+            )}
+          </p>
+          <p>
+            <a
+              className="btn btn-primary"
+              href={`/manage/groups/${subscription._id}/members`}
+            >
+              <i className="fa fa-fw fa-users" /> {t('manage_members')}
+            </a>
+          </p>
+          <p>
+            <a href={`/manage/groups/${subscription._id}/managers`}>
+              <i className="fa fa-fw fa-users" /> {t('manage_group_managers')}
+            </a>
+          </p>
+          <p>
+            <a href={`/metrics/groups/${subscription._id}`}>
+              <i className="fa fa-fw fa-line-chart" /> {t('view_metrics')}
+            </a>
+          </p>
+          <hr />
+        </div>
+      ))}
+    </>
+  )
+}

+ 1 - 0
services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx

@@ -77,6 +77,7 @@ function PersonalSubscription({
           <strong>{t('payment_provider_unreachable_error')}</strong>
         </div>
       )}
+      <hr />
     </>
   )
 }

+ 9 - 2
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 (
     <div className="container">
@@ -21,8 +25,11 @@ function SubscriptionDashboard() {
               <h1>{t('your_subscription')}</h1>
             </div>
 
-            <InstitutionMemberships memberships={institutionMemberships} />
             <PersonalSubscription subscription={subscription} />
+            <ManagedGroupSubscriptions
+              subscriptions={managedGroupSubscriptions}
+            />
+            <InstitutionMemberships memberships={institutionMemberships} />
             {!hasDisplayedSubscription && <FreePlan />}
           </div>
         </div>

+ 3 - 0
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",

+ 94 - 0
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('<ManagedGroupSubscriptions />', function () {
+  beforeEach(function () {
+    window.metaAttributesCache = new Map()
+  })
+
+  afterEach(function () {
+    window.metaAttributesCache = new Map()
+  })
+
+  it('renders all managed group subscriptions', function () {
+    render(
+      <ManagedGroupSubscriptions subscriptions={managedGroupSubscriptions} />
+    )
+
+    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(<ManagedGroupSubscriptions subscriptions={undefined} />)
+    const elements = screen.queryAllByText('You are a', {
+      exact: false,
+    })
+    expect(elements.length).to.equal(0)
+  })
+})

+ 69 - 63
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',
+    _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',
+    },
+    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',
+        has_past_due_invoice: {
+          _: 'false',
+          $: {
+            type: 'boolean',
+          },
         },
       },
+      displayPrice: '$2967.00',
+      currentPlanDisplayPrice: '$2709.00',
+      pendingAdditionalLicenses: 13,
+      pendingTotalLicenses: 23,
     },
-    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',
-  },
-}
+    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'],

+ 4 - 0
services/web/types/subscription/dashboard/subscription.ts

@@ -62,3 +62,7 @@ export type Subscription = {
   }
   pendingPlan?: Plan
 }
+
+export type GroupSubscription = Subscription & {
+  teamName: string
+}