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

[web] Managed groups in React subscription dash

GitOrigin-RevId: 4811d8dd2b42fa9ad83b5c4f12582e7bc04bad40
This commit is contained in:
Jessica Lawshe 2023-01-31 09:42:52 -06:00 committed by Copybot
parent af818e9859
commit b7108f7874
10 changed files with 287 additions and 77 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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": "",

View file

@ -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>
))}
</>
)
}

View file

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

View file

@ -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>

View file

@ -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",

View file

@ -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)
})
})

View file

@ -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'],

View file

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