mirror of
https://github.com/overleaf/overleaf.git
synced 2024-12-27 11:22:16 +00:00
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:
parent
af818e9859
commit
b7108f7874
10 changed files with 287 additions and 77 deletions
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -77,6 +77,7 @@ function PersonalSubscription({
|
|||
<strong>{t('payment_provider_unreachable_error')}</strong>
|
||||
</div>
|
||||
)}
|
||||
<hr />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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'],
|
||||
|
|
|
@ -62,3 +62,7 @@ export type Subscription = {
|
|||
}
|
||||
pendingPlan?: Plan
|
||||
}
|
||||
|
||||
export type GroupSubscription = Subscription & {
|
||||
teamName: string
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue