mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-11 10:44:06 +00:00
Merge pull request #11862 from overleaf/ab-group-subscription-memberships-dash-react
[web] Migrate group subscription memberships to React dash GitOrigin-RevId: d5ff3ae4e5d8c422530502af22edda6c24c9a593
This commit is contained in:
parent
f4a9b7bf60
commit
f86eeac522
11 changed files with 346 additions and 11 deletions
services/web
app/views/subscriptions
frontend
extracted-translations.json
js
features/subscription
components/dashboard
group-subscription-membership.tsxgroup-subscription-memberships.tsxleave-group-modal.tsxsubscription-dashboard.tsx
context
main
test/frontend/features/subscription/components/dashboard
types/subscription/dashboard
|
@ -8,6 +8,7 @@ block head-scripts
|
|||
|
||||
block append meta
|
||||
meta(name="ol-managedGroupSubscriptions", data-type="json" content=managedGroupSubscriptions)
|
||||
meta(name="ol-memberGroupSubscriptions", data-type="json" content=memberGroupSubscriptions)
|
||||
meta(name="ol-managedInstitutions", data-type="json", content=managedInstitutions)
|
||||
meta(name="ol-managedPublishers", data-type="json" content=managedPublishers)
|
||||
meta(name="ol-planCodesChangingAtTermEnd", data-type="json", content=planCodesChangingAtTermEnd)
|
||||
|
|
|
@ -410,6 +410,8 @@
|
|||
"learn_more": "",
|
||||
"learn_more_about_link_sharing": "",
|
||||
"leave": "",
|
||||
"leave_group": "",
|
||||
"leave_now": "",
|
||||
"leave_projects": "",
|
||||
"let_us_know": "",
|
||||
"license_for_educational_purposes": "",
|
||||
|
@ -759,6 +761,7 @@
|
|||
"sure_you_want_to_cancel_plan_change": "",
|
||||
"sure_you_want_to_change_plan": "",
|
||||
"sure_you_want_to_delete": "",
|
||||
"sure_you_want_to_leave_group": "",
|
||||
"switch_to_editor": "",
|
||||
"switch_to_pdf": "",
|
||||
"symbol_palette": "",
|
||||
|
@ -909,6 +912,7 @@
|
|||
"you_are_a_manager_of_publisher_x": "",
|
||||
"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_are_on_x_plan_as_member_of_group_subscription_y_administered_by_z": "",
|
||||
"you_can_now_log_in_sso": "",
|
||||
"you_dont_have_any_repositories": "",
|
||||
"you_have_added_x_of_group_size_y": "",
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { Button } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { MemberGroupSubscription } from '../../../../../../types/subscription/dashboard/subscription'
|
||||
import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
|
||||
import { LEAVE_GROUP_MODAL_ID } from './leave-group-modal'
|
||||
import PremiumFeaturesLink from './premium-features-link'
|
||||
|
||||
type GroupSubscriptionMembershipProps = {
|
||||
subscription: MemberGroupSubscription
|
||||
isLast: boolean
|
||||
}
|
||||
|
||||
export default function GroupSubscriptionMembership({
|
||||
subscription,
|
||||
isLast,
|
||||
}: GroupSubscriptionMembershipProps) {
|
||||
const { t } = useTranslation()
|
||||
const { handleOpenModal, setLeavingGroupId } =
|
||||
useSubscriptionDashboardContext()
|
||||
|
||||
const leaveGroup = () => {
|
||||
handleOpenModal(LEAVE_GROUP_MODAL_ID)
|
||||
setLeavingGroupId(subscription._id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="you_are_on_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>
|
||||
{subscription.teamNotice && (
|
||||
<p>
|
||||
{/* Team notice is sanitized in SubscriptionViewModelBuilder */}
|
||||
<em>{subscription.teamNotice}</em>
|
||||
</p>
|
||||
)}
|
||||
{isLast && <PremiumFeaturesLink />}
|
||||
<span>
|
||||
<Button bsStyle="danger" onClick={leaveGroup}>
|
||||
{t('leave_group')}
|
||||
</Button>
|
||||
</span>
|
||||
<hr />
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import { MemberGroupSubscription } from '../../../../../../types/subscription/dashboard/subscription'
|
||||
import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
|
||||
import GroupSubscriptionMembership from './group-subscription-membership'
|
||||
import LeaveGroupModal from './leave-group-modal'
|
||||
|
||||
export default function GroupSubscriptionMemberships() {
|
||||
const { memberGroupSubscriptions } = useSubscriptionDashboardContext()
|
||||
|
||||
if (!memberGroupSubscriptions) {
|
||||
return null
|
||||
}
|
||||
|
||||
const memberOnlyGroupSubscriptions = memberGroupSubscriptions.filter(
|
||||
subscription => !subscription.userIsGroupManager
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{memberOnlyGroupSubscriptions.map(
|
||||
(subscription: MemberGroupSubscription, index: number) => (
|
||||
<GroupSubscriptionMembership
|
||||
subscription={subscription}
|
||||
isLast={index === memberOnlyGroupSubscriptions.length - 1}
|
||||
key={subscription._id}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
<LeaveGroupModal />
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
import { Button, Modal } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { deleteJSON } from '../../../../infrastructure/fetch-json'
|
||||
import AccessibleModal from '../../../../shared/components/accessible-modal'
|
||||
import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
|
||||
|
||||
export const LEAVE_GROUP_MODAL_ID = 'leave-group'
|
||||
|
||||
export default function LeaveGroupModal() {
|
||||
const { t } = useTranslation()
|
||||
const { handleCloseModal, modalIdShown, leavingGroupId } =
|
||||
useSubscriptionDashboardContext()
|
||||
const [inflight, setInflight] = useState(false)
|
||||
|
||||
const handleConfirmLeaveGroup = useCallback(async () => {
|
||||
if (!leavingGroupId) {
|
||||
return
|
||||
}
|
||||
setInflight(true)
|
||||
try {
|
||||
const params = new URLSearchParams()
|
||||
params.set('subscriptionId', leavingGroupId)
|
||||
await deleteJSON(`/subscription/group/user?${params}`)
|
||||
window.location.reload()
|
||||
} catch (error) {
|
||||
console.log('something went wrong', error)
|
||||
setInflight(false)
|
||||
}
|
||||
}, [leavingGroupId])
|
||||
|
||||
if (modalIdShown !== LEAVE_GROUP_MODAL_ID || !leavingGroupId) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleModal
|
||||
id={LEAVE_GROUP_MODAL_ID}
|
||||
show
|
||||
animation
|
||||
onHide={handleCloseModal}
|
||||
backdrop="static"
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{t('leave_group')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Body>
|
||||
<p>{t('sure_you_want_to_leave_group')}</p>
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="btn-secondary"
|
||||
onClick={handleCloseModal}
|
||||
disabled={inflight}
|
||||
>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
bsStyle="danger"
|
||||
onClick={handleConfirmLeaveGroup}
|
||||
disabled={inflight}
|
||||
>
|
||||
{inflight ? t('processing_uppercase') + '…' : t('leave_now')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</AccessibleModal>
|
||||
)
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import GroupSubscriptionMemberships from './group-subscription-memberships'
|
||||
import InstitutionMemberships from './institution-memberships'
|
||||
import FreePlan from './free-plan'
|
||||
import ManagedPublishers from './managed-publishers'
|
||||
|
@ -24,6 +25,7 @@ function SubscriptionDashboard() {
|
|||
<ManagedGroupSubscriptions />
|
||||
<ManagedInstitutions />
|
||||
<ManagedPublishers />
|
||||
<GroupSubscriptionMemberships />
|
||||
<InstitutionMemberships />
|
||||
{!hasDisplayedSubscription && <FreePlan />}
|
||||
</div>
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from 'react'
|
||||
import {
|
||||
ManagedGroupSubscription,
|
||||
MemberGroupSubscription,
|
||||
Subscription,
|
||||
} from '../../../../../types/subscription/dashboard/subscription'
|
||||
import {
|
||||
|
@ -40,6 +41,7 @@ type SubscriptionDashboardContextValue = {
|
|||
hasDisplayedSubscription: boolean
|
||||
institutionMemberships?: Institution[]
|
||||
managedGroupSubscriptions: ManagedGroupSubscription[]
|
||||
memberGroupSubscriptions: MemberGroupSubscription[]
|
||||
managedInstitutions: ManagedInstitution[]
|
||||
managedPublishers: ManagedPublisher[]
|
||||
updateManagedInstitution: (institution: ManagedInstitution) => void
|
||||
|
@ -64,6 +66,8 @@ type SubscriptionDashboardContextValue = {
|
|||
setShowCancellation: React.Dispatch<React.SetStateAction<boolean>>
|
||||
showChangePersonalPlan: boolean
|
||||
setShowChangePersonalPlan: React.Dispatch<React.SetStateAction<boolean>>
|
||||
leavingGroupId?: string
|
||||
setLeavingGroupId: React.Dispatch<React.SetStateAction<string | undefined>>
|
||||
}
|
||||
|
||||
export const SubscriptionDashboardContext = createContext<
|
||||
|
@ -100,23 +104,33 @@ export function SubscriptionDashboardProvider({
|
|||
useState<PriceForDisplayData>()
|
||||
const [groupPlanToChangeToPriceError, setGroupPlanToChangeToPriceError] =
|
||||
useState(false)
|
||||
const [leavingGroupId, setLeavingGroupId] = useState<string | undefined>()
|
||||
|
||||
const plansWithoutDisplayPrice = getMeta('ol-plans')
|
||||
const institutionMemberships = getMeta('ol-currentInstitutionsWithLicence')
|
||||
const personalSubscription = getMeta('ol-subscription')
|
||||
const managedGroupSubscriptions = getMeta('ol-managedGroupSubscriptions')
|
||||
const institutionMemberships: Institution[] = getMeta(
|
||||
'ol-currentInstitutionsWithLicence'
|
||||
)
|
||||
const personalSubscription: Subscription = getMeta('ol-subscription')
|
||||
const managedGroupSubscriptions: ManagedGroupSubscription[] = getMeta(
|
||||
'ol-managedGroupSubscriptions'
|
||||
)
|
||||
const memberGroupSubscriptions: MemberGroupSubscription[] = getMeta(
|
||||
'ol-memberGroupSubscriptions'
|
||||
)
|
||||
const [managedInstitutions, setManagedInstitutions] = useState<
|
||||
ManagedInstitution[]
|
||||
>(getMeta('ol-managedInstitutions'))
|
||||
const managedPublishers = getMeta('ol-managedPublishers')
|
||||
const recurlyApiKey = getMeta('ol-recurlyApiKey')
|
||||
|
||||
const hasDisplayedSubscription =
|
||||
const hasDisplayedSubscription = Boolean(
|
||||
institutionMemberships?.length > 0 ||
|
||||
personalSubscription ||
|
||||
managedGroupSubscriptions?.length > 0 ||
|
||||
managedInstitutions?.length > 0 ||
|
||||
managedPublishers?.length > 0
|
||||
personalSubscription ||
|
||||
memberGroupSubscriptions?.length > 0 ||
|
||||
managedGroupSubscriptions?.length > 0 ||
|
||||
managedInstitutions?.length > 0 ||
|
||||
managedPublishers?.length > 0
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRecurlyLoaded()) {
|
||||
|
@ -227,6 +241,7 @@ export function SubscriptionDashboardProvider({
|
|||
hasDisplayedSubscription,
|
||||
institutionMemberships,
|
||||
managedGroupSubscriptions,
|
||||
memberGroupSubscriptions,
|
||||
managedInstitutions,
|
||||
managedPublishers,
|
||||
updateManagedInstitution,
|
||||
|
@ -247,6 +262,8 @@ export function SubscriptionDashboardProvider({
|
|||
setShowCancellation,
|
||||
showChangePersonalPlan,
|
||||
setShowChangePersonalPlan,
|
||||
leavingGroupId,
|
||||
setLeavingGroupId,
|
||||
}),
|
||||
[
|
||||
groupPlanToChangeToCode,
|
||||
|
@ -259,6 +276,7 @@ export function SubscriptionDashboardProvider({
|
|||
hasDisplayedSubscription,
|
||||
institutionMemberships,
|
||||
managedGroupSubscriptions,
|
||||
memberGroupSubscriptions,
|
||||
managedInstitutions,
|
||||
managedPublishers,
|
||||
updateManagedInstitution,
|
||||
|
@ -279,6 +297,8 @@ export function SubscriptionDashboardProvider({
|
|||
setShowCancellation,
|
||||
showChangePersonalPlan,
|
||||
setShowChangePersonalPlan,
|
||||
leavingGroupId,
|
||||
setLeavingGroupId,
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
@ -335,7 +335,7 @@ App.controller(
|
|||
_csrf: window.csrfToken,
|
||||
},
|
||||
})
|
||||
.then(() => location.reload())
|
||||
.then(() => window.location.reload())
|
||||
.catch(() => console.log('something went wrong changing plan'))
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
import { expect } from 'chai'
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from '@testing-library/react'
|
||||
import sinon from 'sinon'
|
||||
import GroupSubscriptionMemberships from '../../../../../../frontend/js/features/subscription/components/dashboard/group-subscription-memberships'
|
||||
import { SubscriptionDashboardProvider } from '../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import { MemberGroupSubscription } from '../../../../../../types/subscription/dashboard/subscription'
|
||||
import {
|
||||
groupActiveSubscription,
|
||||
groupActiveSubscriptionWithPendingLicenseChange,
|
||||
} from '../../fixtures/subscriptions'
|
||||
|
||||
const userId = 'fff999fff999'
|
||||
const memberGroupSubscriptions: MemberGroupSubscription[] = [
|
||||
{
|
||||
...groupActiveSubscription,
|
||||
userIsGroupManager: false,
|
||||
planLevelName: 'Professional',
|
||||
admin_id: {
|
||||
id: 'abc123abc123',
|
||||
email: 'you@example.com',
|
||||
},
|
||||
},
|
||||
{
|
||||
...groupActiveSubscriptionWithPendingLicenseChange,
|
||||
userIsGroupManager: true,
|
||||
planLevelName: 'Collaborator',
|
||||
admin_id: {
|
||||
id: 'bcd456bcd456',
|
||||
email: 'someone@example.com',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
describe('<GroupSubscriptionMemberships />', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
window.metaAttributesCache.set(
|
||||
'ol-memberGroupSubscriptions',
|
||||
memberGroupSubscriptions
|
||||
)
|
||||
window.user_id = userId
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
delete window.user_id
|
||||
fetchMock.reset()
|
||||
})
|
||||
|
||||
it('renders all group subscriptions not managed', function () {
|
||||
render(
|
||||
<SubscriptionDashboardProvider>
|
||||
<GroupSubscriptionMemberships />
|
||||
</SubscriptionDashboardProvider>
|
||||
)
|
||||
|
||||
const elements = screen.getAllByText('You are on our', {
|
||||
exact: false,
|
||||
})
|
||||
expect(elements.length).to.equal(1)
|
||||
expect(elements[0].textContent).to.equal(
|
||||
'You are on our Professional plan as a member of the group subscription GAS administered by you@example.com'
|
||||
)
|
||||
})
|
||||
|
||||
describe('opens leave group modal when button is clicked', function () {
|
||||
let reloadStub: () => void
|
||||
const originalLocation = window.location
|
||||
|
||||
beforeEach(function () {
|
||||
reloadStub = sinon.stub()
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { reload: reloadStub },
|
||||
})
|
||||
|
||||
render(
|
||||
<SubscriptionDashboardProvider>
|
||||
<GroupSubscriptionMemberships />
|
||||
</SubscriptionDashboardProvider>
|
||||
)
|
||||
|
||||
const leaveGroupButton = screen.getByText('Leave group')
|
||||
fireEvent.click(leaveGroupButton)
|
||||
|
||||
this.confirmModal = screen.getByRole('dialog')
|
||||
within(this.confirmModal).getByText(
|
||||
'Are you sure you want to leave this group?'
|
||||
)
|
||||
|
||||
this.cancelButton = within(this.confirmModal).getByText('Cancel')
|
||||
this.leaveNowButton = within(this.confirmModal).getByText('Leave now')
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: originalLocation,
|
||||
})
|
||||
})
|
||||
|
||||
it('close the modal', function () {
|
||||
fireEvent.click(this.cancelButton)
|
||||
expect(screen.queryByRole('dialog')).to.not.exist
|
||||
})
|
||||
|
||||
it('leave the group', async function () {
|
||||
const leaveGroupApiMock = fetchMock.delete(
|
||||
`/subscription/group/user?subscriptionId=bcd567`,
|
||||
{
|
||||
status: 204,
|
||||
}
|
||||
)
|
||||
|
||||
fireEvent.click(this.leaveNowButton)
|
||||
|
||||
expect(leaveGroupApiMock.called()).to.be.true
|
||||
await waitFor(() => {
|
||||
expect(reloadStub).to.have.been.called
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('renders nothing when there are no group subscriptions', function () {
|
||||
window.metaAttributesCache.set('ol-memberGroupSubscriptions', undefined)
|
||||
|
||||
render(
|
||||
<SubscriptionDashboardProvider>
|
||||
<GroupSubscriptionMemberships />
|
||||
</SubscriptionDashboardProvider>
|
||||
)
|
||||
const elements = screen.queryAllByText('You are on our', {
|
||||
exact: false,
|
||||
})
|
||||
expect(elements.length).to.equal(0)
|
||||
})
|
||||
})
|
|
@ -2,3 +2,4 @@ export type SubscriptionDashModalIds =
|
|||
| 'change-to-plan'
|
||||
| 'change-to-group'
|
||||
| 'keep-current-plan'
|
||||
| 'leave-group'
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { CurrencyCode } from '../../../frontend/js/features/subscription/data/currency'
|
||||
import { Nullable } from '../../utils'
|
||||
import { Plan } from '../plan'
|
||||
import { User } from '../../../types/user'
|
||||
|
@ -24,7 +25,7 @@ export type Subscription = {
|
|||
additionalLicenses: number
|
||||
totalLicenses: number
|
||||
nextPaymentDueAt: string
|
||||
currency: string
|
||||
currency: CurrencyCode
|
||||
state?: SubscriptionState
|
||||
trialEndsAtFormatted: Nullable<string>
|
||||
trial_ends_at: Nullable<string>
|
||||
|
@ -53,7 +54,8 @@ export type Subscription = {
|
|||
}
|
||||
|
||||
export type GroupSubscription = Subscription & {
|
||||
teamName: string
|
||||
teamName?: string
|
||||
teamNotice?: string
|
||||
}
|
||||
|
||||
export type ManagedGroupSubscription = Omit<GroupSubscription, 'admin_id'> & {
|
||||
|
@ -61,3 +63,9 @@ export type ManagedGroupSubscription = Omit<GroupSubscription, 'admin_id'> & {
|
|||
planLevelName: string
|
||||
admin_id: User
|
||||
}
|
||||
|
||||
export type MemberGroupSubscription = Omit<GroupSubscription, 'admin_id'> & {
|
||||
userIsGroupManager: boolean
|
||||
planLevelName: string
|
||||
admin_id: User
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue