mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-31 04:41:32 +02:00
Merge pull request #26968 from overleaf/em-revert-jul8
Revert bad deploy GitOrigin-RevId: fd6227cf4fde7fd8053b47365154d59d15fa115e
This commit is contained in:
@@ -26,8 +26,6 @@ class SubtotalLimitExceededError extends OError {}
|
||||
|
||||
class HasPastDueInvoiceError extends OError {}
|
||||
|
||||
class HasNoAdditionalLicenseWhenManuallyCollectedError extends OError {}
|
||||
|
||||
class PaymentActionRequiredError extends OError {
|
||||
constructor(info) {
|
||||
super('Payment action required', info)
|
||||
@@ -45,5 +43,4 @@ module.exports = {
|
||||
InactiveError,
|
||||
SubtotalLimitExceededError,
|
||||
HasPastDueInvoiceError,
|
||||
HasNoAdditionalLicenseWhenManuallyCollectedError,
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import SessionManager from '../Authentication/SessionManager.js'
|
||||
import UserAuditLogHandler from '../User/UserAuditLogHandler.js'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
import Modules from '../../infrastructure/Modules.js'
|
||||
import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
|
||||
import UserGetter from '../User/UserGetter.js'
|
||||
import { Subscription } from '../../models/Subscription.js'
|
||||
import { isProfessionalGroupPlan } from './PlansHelper.mjs'
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
InactiveError,
|
||||
SubtotalLimitExceededError,
|
||||
HasPastDueInvoiceError,
|
||||
HasNoAdditionalLicenseWhenManuallyCollectedError,
|
||||
} from './Errors.js'
|
||||
|
||||
/**
|
||||
@@ -151,13 +151,26 @@ async function addSeatsToGroupSubscription(req, res) {
|
||||
await SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice(
|
||||
subscription
|
||||
)
|
||||
await SubscriptionGroupHandler.promises.checkBillingInfoExistence(
|
||||
paymentProviderSubscription,
|
||||
userId
|
||||
)
|
||||
await SubscriptionGroupHandler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
|
||||
paymentProviderSubscription
|
||||
)
|
||||
|
||||
const { variant: flexibleLicensingForManuallyBilledSubscriptionsVariant } =
|
||||
await SplitTestHandler.promises.getAssignment(
|
||||
req,
|
||||
res,
|
||||
'flexible-group-licensing-for-manually-billed-subscriptions'
|
||||
)
|
||||
|
||||
if (flexibleLicensingForManuallyBilledSubscriptionsVariant === 'enabled') {
|
||||
await SubscriptionGroupHandler.promises.checkBillingInfoExistence(
|
||||
paymentProviderSubscription,
|
||||
userId
|
||||
)
|
||||
} else {
|
||||
await SubscriptionGroupHandler.promises.ensureSubscriptionCollectionMethodIsNotManual(
|
||||
paymentProviderSubscription
|
||||
)
|
||||
// Check if the user has missing billing details
|
||||
await Modules.promises.hooks.fire('getPaymentMethod', userId)
|
||||
}
|
||||
|
||||
res.render('subscriptions/add-seats', {
|
||||
subscriptionId: subscription._id,
|
||||
@@ -174,7 +187,7 @@ async function addSeatsToGroupSubscription(req, res) {
|
||||
)
|
||||
}
|
||||
|
||||
if (error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError) {
|
||||
if (error instanceof ManuallyCollectedError) {
|
||||
return res.redirect(
|
||||
'/user/subscription/group/manually-collected-subscription'
|
||||
)
|
||||
@@ -215,10 +228,10 @@ async function previewAddSeatsSubscriptionChange(req, res) {
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof MissingBillingInfoError ||
|
||||
error instanceof ManuallyCollectedError ||
|
||||
error instanceof PendingChangeError ||
|
||||
error instanceof InactiveError ||
|
||||
error instanceof HasPastDueInvoiceError ||
|
||||
error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError
|
||||
error instanceof HasPastDueInvoiceError
|
||||
) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
@@ -258,10 +271,10 @@ async function createAddSeatsSubscriptionChange(req, res) {
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof MissingBillingInfoError ||
|
||||
error instanceof ManuallyCollectedError ||
|
||||
error instanceof PendingChangeError ||
|
||||
error instanceof InactiveError ||
|
||||
error instanceof HasPastDueInvoiceError ||
|
||||
error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError
|
||||
error instanceof HasPastDueInvoiceError
|
||||
) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
@@ -398,6 +411,12 @@ async function manuallyCollectedSubscription(req, res) {
|
||||
const subscription =
|
||||
await SubscriptionLocator.promises.getUsersSubscription(userId)
|
||||
|
||||
await SplitTestHandler.promises.getAssignment(
|
||||
req,
|
||||
res,
|
||||
'flexible-group-licensing-for-manually-billed-subscriptions'
|
||||
)
|
||||
|
||||
res.render('subscriptions/manually-collected-subscription', {
|
||||
groupName: subscription.teamName,
|
||||
})
|
||||
|
||||
@@ -18,7 +18,6 @@ const {
|
||||
PendingChangeError,
|
||||
InactiveError,
|
||||
HasPastDueInvoiceError,
|
||||
HasNoAdditionalLicenseWhenManuallyCollectedError,
|
||||
} = require('./Errors')
|
||||
const EmailHelper = require('../Helpers/EmailHelper')
|
||||
const { InvalidEmailError } = require('../Errors/Errors')
|
||||
@@ -124,22 +123,6 @@ async function ensureSubscriptionHasNoPastDueInvoice(subscription) {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
|
||||
paymentProviderSubscription
|
||||
) {
|
||||
if (
|
||||
paymentProviderSubscription.isCollectionMethodManual &&
|
||||
!paymentProviderSubscription.hasAddOn(MEMBERS_LIMIT_ADD_ON_CODE)
|
||||
) {
|
||||
throw new HasNoAdditionalLicenseWhenManuallyCollectedError(
|
||||
'This subscription is being collected manually has no "additional-license" add-on',
|
||||
{
|
||||
subscription_id: paymentProviderSubscription.id,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function getUsersGroupSubscriptionDetails(userId) {
|
||||
const subscription =
|
||||
await SubscriptionLocator.promises.getUsersSubscription(userId)
|
||||
@@ -487,10 +470,6 @@ module.exports = {
|
||||
ensureSubscriptionHasNoPastDueInvoice: callbackify(
|
||||
ensureSubscriptionHasNoPastDueInvoice
|
||||
),
|
||||
ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual:
|
||||
callbackify(
|
||||
ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual
|
||||
),
|
||||
getTotalConfirmedUsersInGroup: callbackify(getTotalConfirmedUsersInGroup),
|
||||
isUserPartOfGroup: callbackify(isUserPartOfGroup),
|
||||
getGroupPlanUpgradePreview: callbackify(getGroupPlanUpgradePreview),
|
||||
@@ -505,7 +484,6 @@ module.exports = {
|
||||
ensureSubscriptionCollectionMethodIsNotManual,
|
||||
ensureSubscriptionHasNoPendingChanges,
|
||||
ensureSubscriptionHasNoPastDueInvoice,
|
||||
ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual,
|
||||
getTotalConfirmedUsersInGroup,
|
||||
isUserPartOfGroup,
|
||||
getUsersGroupSubscriptionDetails,
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
const { callbackifyAll } = require('@overleaf/promise-utils')
|
||||
const { Subscription } = require('../../models/Subscription')
|
||||
const SubscriptionHelper = require('./SubscriptionHelper')
|
||||
const { DeletedSubscription } = require('../../models/DeletedSubscription')
|
||||
const logger = require('@overleaf/logger')
|
||||
const {
|
||||
@@ -176,8 +175,7 @@ const SubscriptionLocator = {
|
||||
|
||||
const hasActiveGroupSubscription = memberSubscriptions.some(
|
||||
subscription =>
|
||||
subscription.groupPlan &&
|
||||
SubscriptionHelper.getPaidSubscriptionState(subscription) === 'active'
|
||||
subscription.recurlyStatus?.state === 'active' && subscription.groupPlan
|
||||
)
|
||||
if (hasActiveGroupSubscription) {
|
||||
// Member of a group plan
|
||||
@@ -189,8 +187,7 @@ const SubscriptionLocator = {
|
||||
|
||||
if (personalSubscription) {
|
||||
const hasActivePersonalSubscription =
|
||||
SubscriptionHelper.getPaidSubscriptionState(personalSubscription) ===
|
||||
'active'
|
||||
personalSubscription.recurlyStatus?.state === 'active'
|
||||
if (hasActivePersonalSubscription) {
|
||||
if (personalSubscription.groupPlan) {
|
||||
// Owner of a group plan
|
||||
|
||||
@@ -79,7 +79,11 @@ async function getPopulatedListOfMembers(entity, attributes) {
|
||||
}
|
||||
}
|
||||
|
||||
const users = await UserMembershipViewModel.promises.buildAsync(userObjects)
|
||||
const users = await Promise.all(
|
||||
userObjects.map(userObject =>
|
||||
UserMembershipViewModel.promises.buildAsync(userObject)
|
||||
)
|
||||
)
|
||||
|
||||
for (const user of users) {
|
||||
if (
|
||||
|
||||
@@ -23,59 +23,30 @@ const UserMembershipViewModel = {
|
||||
}
|
||||
},
|
||||
|
||||
buildAsync(userOrIdOrEmailArray, callback) {
|
||||
buildAsync(userOrIdOrEmail, callback) {
|
||||
if (callback == null) {
|
||||
callback = function () {}
|
||||
}
|
||||
if (!isObjectIdInstance(userOrIdOrEmail)) {
|
||||
// userOrIdOrEmail is a user or an email and can be parsed by #build
|
||||
return callback(null, UserMembershipViewModel.build(userOrIdOrEmail))
|
||||
}
|
||||
|
||||
const userObjectIds = userOrIdOrEmailArray.filter(isObjectIdInstance)
|
||||
|
||||
return UserGetter.getUsers(
|
||||
userObjectIds,
|
||||
{
|
||||
email: 1,
|
||||
first_name: 1,
|
||||
last_name: 1,
|
||||
lastLoggedIn: 1,
|
||||
lastActive: 1,
|
||||
enrollment: 1,
|
||||
},
|
||||
function (error, users) {
|
||||
const results = []
|
||||
|
||||
if (error != null) {
|
||||
userOrIdOrEmailArray.forEach(item => {
|
||||
if (isObjectIdInstance(item)) {
|
||||
results.push(buildUserViewModelWithId(item.toString()))
|
||||
} else {
|
||||
// `item` is a user or an email and can be parsed by #build
|
||||
results.push(UserMembershipViewModel.build(item))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const usersMap = new Map()
|
||||
for (const user of users) {
|
||||
usersMap.set(user._id.toString(), user)
|
||||
}
|
||||
|
||||
userOrIdOrEmailArray.forEach(item => {
|
||||
if (isObjectIdInstance(item)) {
|
||||
const user = usersMap.get(item.toString())
|
||||
if (user == null) {
|
||||
results.push(buildUserViewModelWithId(item.toString()))
|
||||
} else {
|
||||
results.push(buildUserViewModel(user))
|
||||
}
|
||||
} else {
|
||||
// `item` is a user or an email and can be parsed by #build
|
||||
results.push(UserMembershipViewModel.build(item))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
callback(null, results)
|
||||
const userId = userOrIdOrEmail
|
||||
const projection = {
|
||||
email: 1,
|
||||
first_name: 1,
|
||||
last_name: 1,
|
||||
lastLoggedIn: 1,
|
||||
lastActive: 1,
|
||||
enrollment: 1,
|
||||
}
|
||||
return UserGetter.getUser(userId, projection, function (error, user) {
|
||||
if (error != null || user == null) {
|
||||
return callback(null, buildUserViewModelWithId(userId.toString()))
|
||||
}
|
||||
)
|
||||
return callback(null, buildUserViewModel(user))
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -872,7 +872,8 @@
|
||||
"is_email_affiliated": "",
|
||||
"issued_on": "",
|
||||
"it_looks_like_that_didnt_work_you_can_try_again_or_get_in_touch": "",
|
||||
"it_looks_like_your_account_is_billed_manually_purchasing_additional_license_or_upgrading_subscription": "",
|
||||
"it_looks_like_your_account_is_billed_manually": "",
|
||||
"it_looks_like_your_account_is_billed_manually_upgrading_subscription": "",
|
||||
"it_looks_like_your_payment_details_are_missing_please_update_your_billing_information": "",
|
||||
"italics": "",
|
||||
"join_beta_program": "",
|
||||
@@ -1025,7 +1026,6 @@
|
||||
"math_inline": "",
|
||||
"maximum_files_uploaded_together": "",
|
||||
"maybe_later": "",
|
||||
"members_added": "",
|
||||
"members_management": "",
|
||||
"mendeley_dynamic_sync_description": "",
|
||||
"mendeley_groups_loading_error": "",
|
||||
@@ -1146,10 +1146,10 @@
|
||||
"on_free_plan_upgrade_to_access_features": "",
|
||||
"one_step_away_from_professional_features": "",
|
||||
"only_group_admin_or_managers_can_delete_your_account_1": "",
|
||||
"only_group_admin_or_managers_can_delete_your_account_10": "",
|
||||
"only_group_admin_or_managers_can_delete_your_account_3": "",
|
||||
"only_group_admin_or_managers_can_delete_your_account_6": "",
|
||||
"only_group_admin_or_managers_can_delete_your_account_7": "",
|
||||
"only_group_admin_or_managers_can_delete_your_account_8": "",
|
||||
"only_group_admin_or_managers_can_delete_your_account_9": "",
|
||||
"only_importer_can_refresh": "",
|
||||
"open_action_menu": "",
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from '../../../../../../types/subscription/subscription-change-preview'
|
||||
import { MergeAndOverride, Nullable } from '../../../../../../types/utils'
|
||||
import { sendMB } from '../../../../infrastructure/event-tracking'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
export const MAX_NUMBER_OF_USERS = 20
|
||||
export const MAX_NUMBER_OF_PO_NUMBER_CHARACTERS = 50
|
||||
@@ -49,6 +50,9 @@ function AddSeats() {
|
||||
const [addSeatsInputError, setAddSeatsInputError] = useState<string>()
|
||||
const [poNumberInputError, setPoNumberInputError] = useState<string>()
|
||||
const [shouldContactSales, setShouldContactSales] = useState(false)
|
||||
const isFlexibleGroupLicensingForManuallyBilledSubscriptions = useFeatureFlag(
|
||||
'flexible-group-licensing-for-manually-billed-subscriptions'
|
||||
)
|
||||
const controller = useAbortController()
|
||||
const { signal: addSeatsSignal } = useAbortController()
|
||||
const { signal: contactSalesSignal } = useAbortController()
|
||||
@@ -369,12 +373,13 @@ function AddSeats() {
|
||||
<FormText type="error">{addSeatsInputError}</FormText>
|
||||
)}
|
||||
</FormGroup>
|
||||
{isCollectionMethodManual && (
|
||||
<PoNumber
|
||||
error={poNumberInputError}
|
||||
validate={validatePoNumber}
|
||||
/>
|
||||
)}
|
||||
{isFlexibleGroupLicensingForManuallyBilledSubscriptions &&
|
||||
isCollectionMethodManual && (
|
||||
<PoNumber
|
||||
error={poNumberInputError}
|
||||
validate={validatePoNumber}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<CostSummarySection
|
||||
isLoadingCostSummary={isLoadingCostSummary}
|
||||
|
||||
@@ -13,7 +13,6 @@ import OLCard from '@/features/ui/components/ol/ol-card'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
|
||||
import OLFormText from '@/features/ui/components/ol/ol-form-text'
|
||||
import OLNotification from '@/features/ui/components/ol/ol-notification'
|
||||
|
||||
export default function GroupMembers() {
|
||||
const { isReady } = useWaitForI18n()
|
||||
@@ -27,7 +26,6 @@ export default function GroupMembers() {
|
||||
removeMemberError,
|
||||
inviteMemberLoading,
|
||||
inviteError,
|
||||
memberAdded,
|
||||
paths,
|
||||
} = useGroupMembersContext()
|
||||
const [emailString, setEmailString] = useState<string>('')
|
||||
@@ -142,13 +140,6 @@ export default function GroupMembers() {
|
||||
data-testid="add-more-members-form"
|
||||
>
|
||||
<p className="small">{t('invite_more_members')}</p>
|
||||
{memberAdded && (
|
||||
<OLNotification
|
||||
content={t('members_added')}
|
||||
type="success"
|
||||
className="mt-2 mb-3"
|
||||
/>
|
||||
)}
|
||||
<ErrorAlert error={inviteError} />
|
||||
<form onSubmit={onAddMembersSubmit}>
|
||||
<OLRow>
|
||||
|
||||
@@ -2,9 +2,14 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||
import OLNotification from '@/features/ui/components/ol/ol-notification'
|
||||
import Card from '@/features/group-management/components/card'
|
||||
import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
|
||||
import { useFeatureFlag } from '@/shared/context/split-test-context'
|
||||
|
||||
function ManuallyCollectedSubscription() {
|
||||
const { t } = useTranslation()
|
||||
const isFlexibleGroupLicensingForManuallyBilledSubscriptions = useFeatureFlag(
|
||||
'flexible-group-licensing-for-manually-billed-subscriptions'
|
||||
)
|
||||
|
||||
const { isReady } = useWaitForI18n()
|
||||
|
||||
if (!isReady) {
|
||||
@@ -17,13 +22,23 @@ function ManuallyCollectedSubscription() {
|
||||
type="error"
|
||||
title={t('account_billed_manually')}
|
||||
content={
|
||||
<Trans
|
||||
i18nKey="it_looks_like_your_account_is_billed_manually_purchasing_additional_license_or_upgrading_subscription"
|
||||
components={[
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
|
||||
<a href="/contact" rel="noreferrer noopener" />,
|
||||
]}
|
||||
/>
|
||||
isFlexibleGroupLicensingForManuallyBilledSubscriptions ? (
|
||||
<Trans
|
||||
i18nKey="it_looks_like_your_account_is_billed_manually_upgrading_subscription"
|
||||
components={[
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
|
||||
<a href="/contact" rel="noreferrer noopener" />,
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey="it_looks_like_your_account_is_billed_manually"
|
||||
components={[
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
|
||||
<a href="/contact" rel="noreferrer noopener" />,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
className="m-0"
|
||||
/>
|
||||
|
||||
@@ -30,6 +30,9 @@ export default function ManagedUserStatus({ user }: ManagedUserStatusProps) {
|
||||
</span>
|
||||
)
|
||||
|
||||
if (user.isEntityAdmin) {
|
||||
return <span className="security-state-group-admin" />
|
||||
}
|
||||
if (user.invite) {
|
||||
return managedUserInvite
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef, useEffect, useMemo } from 'react'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { User } from '../../../../../../types/group-management/user'
|
||||
import { useGroupMembersContext } from '../../context/group-members-context'
|
||||
@@ -13,9 +13,6 @@ import getMeta from '@/utils/meta'
|
||||
import UnlinkUserModal from './unlink-user-modal'
|
||||
import OLTable from '@/features/ui/components/ol/ol-table'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
import Pagination from '@/shared/components/pagination'
|
||||
|
||||
const USERS_DISPLAY_LIMIT = 50
|
||||
|
||||
type ManagedUsersListProps = {
|
||||
groupId: string
|
||||
@@ -34,30 +31,13 @@ export default function MembersList({ groupId }: ManagedUsersListProps) {
|
||||
const managedUsersActive = getMeta('ol-managedUsersActive')
|
||||
const groupSSOActive = getMeta('ol-groupSSOActive')
|
||||
const tHeadRowRef = useRef<HTMLTableRowElement>(null)
|
||||
const [pagination, setPagination] = useState({ currPage: 1, totalPages: 1 })
|
||||
|
||||
const usersForCurrentPage = useMemo(
|
||||
() =>
|
||||
users.slice(
|
||||
(pagination.currPage - 1) * USERS_DISPLAY_LIMIT,
|
||||
pagination.currPage * USERS_DISPLAY_LIMIT
|
||||
),
|
||||
[users, pagination.currPage]
|
||||
)
|
||||
|
||||
const handlePageClick = (
|
||||
_e: React.MouseEvent<HTMLButtonElement>,
|
||||
page: number
|
||||
) => {
|
||||
setPagination(p => ({ ...p, currPage: page }))
|
||||
}
|
||||
const [colSpan, setColSpan] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
setPagination(p => ({
|
||||
...p,
|
||||
totalPages: Math.ceil(users.length / USERS_DISPLAY_LIMIT),
|
||||
}))
|
||||
}, [users.length])
|
||||
if (tHeadRowRef.current) {
|
||||
setColSpan(tHeadRowRef.current.querySelectorAll('th').length)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -113,17 +93,12 @@ export default function MembersList({ groupId }: ManagedUsersListProps) {
|
||||
<tbody>
|
||||
{users.length === 0 && (
|
||||
<tr>
|
||||
<td
|
||||
className="text-center"
|
||||
colSpan={
|
||||
tHeadRowRef.current?.querySelectorAll('th').length ?? 0
|
||||
}
|
||||
>
|
||||
<td className="text-center" colSpan={colSpan}>
|
||||
<small>{t('no_members')}</small>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{usersForCurrentPage.map(user => (
|
||||
{users.map(user => (
|
||||
<MemberRow
|
||||
key={user.email}
|
||||
user={user}
|
||||
@@ -136,15 +111,6 @@ export default function MembersList({ groupId }: ManagedUsersListProps) {
|
||||
))}
|
||||
</tbody>
|
||||
</OLTable>
|
||||
{pagination.totalPages > 1 && (
|
||||
<div className="d-flex justify-content-center">
|
||||
<Pagination
|
||||
handlePageClick={handlePageClick}
|
||||
currentPage={pagination.currPage}
|
||||
totalPages={pagination.totalPages}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{userToOffboard && (
|
||||
<OffboardManagedUserModal
|
||||
user={userToOffboard}
|
||||
|
||||
@@ -31,7 +31,6 @@ export type GroupMembersContextValue = {
|
||||
updateMemberView: (userId: string, updatedUser: User) => void
|
||||
inviteMemberLoading: boolean
|
||||
inviteError?: APIError
|
||||
memberAdded: boolean
|
||||
paths: { [key: string]: string }
|
||||
}
|
||||
|
||||
@@ -59,7 +58,6 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
|
||||
const [inviteError, setInviteError] = useState<APIError>()
|
||||
const [removeMemberInflightCount, setRemoveMemberInflightCount] = useState(0)
|
||||
const [removeMemberError, setRemoveMemberError] = useState<APIError>()
|
||||
const [memberAdded, setMemberAdded] = useState(false)
|
||||
|
||||
const groupId = getMeta('ol-groupId')
|
||||
|
||||
@@ -76,9 +74,7 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
|
||||
const addMembers = useCallback(
|
||||
(emailString: string) => {
|
||||
setInviteError(undefined)
|
||||
setMemberAdded(false)
|
||||
const emails = parseEmails(emailString)
|
||||
let isError = false
|
||||
mapSeries(emails, async email => {
|
||||
setInviteUserInflightCount(count => count + 1)
|
||||
try {
|
||||
@@ -98,15 +94,8 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
|
||||
} catch (error: unknown) {
|
||||
debugConsole.error(error)
|
||||
setInviteError((error as FetchError)?.data?.error || {})
|
||||
isError = true
|
||||
}
|
||||
setInviteUserInflightCount(count => {
|
||||
const newCount = count - 1
|
||||
if (newCount === 0 && !isError) {
|
||||
setMemberAdded(true)
|
||||
}
|
||||
return newCount
|
||||
})
|
||||
setInviteUserInflightCount(count => count - 1)
|
||||
})
|
||||
},
|
||||
[paths.addMember, users, setUsers]
|
||||
@@ -184,7 +173,6 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
|
||||
removeMemberError,
|
||||
inviteMemberLoading: inviteUserInflightCount > 0,
|
||||
inviteError,
|
||||
memberAdded,
|
||||
paths,
|
||||
}),
|
||||
[
|
||||
@@ -203,7 +191,6 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
|
||||
removeMemberError,
|
||||
inviteUserInflightCount,
|
||||
inviteError,
|
||||
memberAdded,
|
||||
paths,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -247,7 +247,6 @@ export interface Meta {
|
||||
'ol-splitTestVariants': { [name: string]: string }
|
||||
'ol-ssoDisabled': boolean
|
||||
'ol-ssoErrorMessage': string
|
||||
'ol-stripeAccountId': string
|
||||
'ol-stripeCustomerId': string
|
||||
'ol-subscription': any // TODO: mixed types, split into two fields
|
||||
'ol-subscriptionChangePreview': SubscriptionChangePreview
|
||||
|
||||
@@ -1130,7 +1130,8 @@
|
||||
"issued_on": "Issued: __date__",
|
||||
"it": "Italian",
|
||||
"it_looks_like_that_didnt_work_you_can_try_again_or_get_in_touch": "It looks like that didn’t work. You can try again or <0>get in touch</0> with our Support team for more help.",
|
||||
"it_looks_like_your_account_is_billed_manually_purchasing_additional_license_or_upgrading_subscription": "It looks like your account is being billed manually - purchasing additional licenses or upgrading your subscription can only be done by the Support team. Please <0>get in touch</0> for help.",
|
||||
"it_looks_like_your_account_is_billed_manually": "It looks like your account is being billed manually - adding seats or upgrading your subscription can only be done by the Support team. Please <0>get in touch</0> for help.",
|
||||
"it_looks_like_your_account_is_billed_manually_upgrading_subscription": "It looks like your account is being billed manually - upgrading your subscription can only be done by the Support team. Please <0>get in touch</0> for help.",
|
||||
"it_looks_like_your_payment_details_are_missing_please_update_your_billing_information": "It looks like your payment details are missing. Please <0>update your billing information</0>, or <1>get in touch</1> with our Support team for more help.",
|
||||
"italics": "Italics",
|
||||
"ja": "Japanese",
|
||||
@@ -1341,7 +1342,6 @@
|
||||
"may": "May",
|
||||
"maybe_later": "Maybe later",
|
||||
"member_picker": "Select number of users for group plan",
|
||||
"members_added": "Member(s) added.",
|
||||
"members_management": "Members management",
|
||||
"mendeley": "Mendeley",
|
||||
"mendeley_dynamic_sync_description": "With the Mendeley integration, you can import your references into __appName__. You can either import all your references at once or dynamically search your Mendeley library directly from __appName__.",
|
||||
@@ -1504,10 +1504,10 @@
|
||||
"ongoing_experiments": "Ongoing experiments",
|
||||
"online_latex_editor": "Online LaTeX Editor",
|
||||
"only_group_admin_or_managers_can_delete_your_account_1": "By becoming a managed user, your organization will have admin rights over your account and control over your stuff, including the right to close your account and access, delete and share your stuff. As a result:",
|
||||
"only_group_admin_or_managers_can_delete_your_account_10": "If you have an individual subscription, we’ll automatically terminate it and cancel its renewal when your account becomes managed. To request a pro-rata refund for the remainder, please contact Support.",
|
||||
"only_group_admin_or_managers_can_delete_your_account_3": "Your group admin and group managers will be able to reassign ownership of your projects to another group member.",
|
||||
"only_group_admin_or_managers_can_delete_your_account_6": "Only your group admin or group managers will be able to delete your account or change your account back into an unmanaged account.",
|
||||
"only_group_admin_or_managers_can_delete_your_account_7": "Only your group admin or group managers will be able to delete your account or change your account into an unmanaged account.",
|
||||
"only_group_admin_or_managers_can_delete_your_account_8": "We’ll cancel the renewal of your subscription, reach out to Support to request a pro-rata refund. Your individual subscription will be terminated when your account becomes managed.",
|
||||
"only_group_admin_or_managers_can_delete_your_account_9": "Once you have become a managed user, <0>you yourself cannot change it back to an unmanaged account</0>. <1>Learn more about managed Overleaf accounts.</1>",
|
||||
"only_importer_can_refresh": "Only the person who originally imported this __provider__ file can refresh it.",
|
||||
"open_action_menu": "Open __name__ action menu",
|
||||
|
||||
@@ -14,6 +14,9 @@ describe('<AddSeats />', function () {
|
||||
win.metaAttributesCache.set('ol-totalLicenses', this.totalLicenses)
|
||||
win.metaAttributesCache.set('ol-isProfessional', false)
|
||||
win.metaAttributesCache.set('ol-isCollectionMethodManual', true)
|
||||
win.metaAttributesCache.set('ol-splitTestVariants', {
|
||||
'flexible-group-licensing-for-manually-billed-subscriptions': 'enabled',
|
||||
})
|
||||
})
|
||||
|
||||
cy.mount(
|
||||
|
||||
@@ -61,4 +61,26 @@ describe('MemberStatus', function () {
|
||||
cy.get('.security-state-not-managed').contains('Managed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with the group admin', function () {
|
||||
const user: User = {
|
||||
_id: 'some-user',
|
||||
email: 'some.user@example.com',
|
||||
first_name: 'Some',
|
||||
last_name: 'User',
|
||||
invite: false,
|
||||
last_active_at: new Date(),
|
||||
enrollment: undefined,
|
||||
isEntityAdmin: true,
|
||||
}
|
||||
beforeEach(function () {
|
||||
cy.mount(<ManagedUserStatus user={user} />)
|
||||
})
|
||||
|
||||
it('should render no state indicator', function () {
|
||||
cy.get('.security-state-group-admin')
|
||||
.contains('Managed')
|
||||
.should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -121,16 +121,6 @@ describe('MembersList', function () {
|
||||
users[1].last_name
|
||||
)
|
||||
})
|
||||
it('should render the pagination navigation', function () {
|
||||
cy.window().then(win => {
|
||||
win.metaAttributesCache.set(
|
||||
'ol-users',
|
||||
Array.from({ length: 50 }).flatMap(() => users.flat())
|
||||
)
|
||||
})
|
||||
mountManagedUsersList()
|
||||
cy.findByRole('navigation', { name: /pagination navigation/i })
|
||||
})
|
||||
})
|
||||
|
||||
describe('empty user list', function () {
|
||||
|
||||
@@ -73,8 +73,6 @@ describe('SubscriptionGroupController', function () {
|
||||
.resolves(ctx.previewSubscriptionChangeData),
|
||||
checkBillingInfoExistence: sinon.stub().resolves(ctx.paymentMethod),
|
||||
updateSubscriptionPaymentTerms: sinon.stub().resolves(),
|
||||
ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual:
|
||||
sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -107,6 +105,12 @@ describe('SubscriptionGroupController', function () {
|
||||
},
|
||||
}
|
||||
|
||||
ctx.SplitTestHandler = {
|
||||
promises: {
|
||||
getAssignment: sinon.stub().resolves({ variant: 'enabled' }),
|
||||
},
|
||||
}
|
||||
|
||||
ctx.UserGetter = {
|
||||
promises: {
|
||||
getUserEmail: sinon.stub().resolves(ctx.user.email),
|
||||
@@ -136,7 +140,6 @@ describe('SubscriptionGroupController', function () {
|
||||
InactiveError: class extends Error {},
|
||||
SubtotalLimitExceededError: class extends Error {},
|
||||
HasPastDueInvoiceError: class extends Error {},
|
||||
HasNoAdditionalLicenseWhenManuallyCollectedError: class extends Error {},
|
||||
}
|
||||
|
||||
vi.doMock(
|
||||
@@ -168,6 +171,13 @@ describe('SubscriptionGroupController', function () {
|
||||
default: ctx.Modules,
|
||||
}))
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/SplitTests/SplitTestHandler',
|
||||
() => ({
|
||||
default: ctx.SplitTestHandler,
|
||||
})
|
||||
)
|
||||
|
||||
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
|
||||
default: ctx.UserGetter,
|
||||
}))
|
||||
@@ -446,9 +456,6 @@ describe('SubscriptionGroupController', function () {
|
||||
ctx.SubscriptionGroupHandler.promises.checkBillingInfoExistence
|
||||
.calledWith(ctx.recurlySubscription, ctx.adminUserId)
|
||||
.should.equal(true)
|
||||
ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual
|
||||
.calledWith(ctx.recurlySubscription)
|
||||
.should.equal(true)
|
||||
page.should.equal('subscriptions/add-seats')
|
||||
props.subscriptionId.should.equal(ctx.subscriptionId)
|
||||
props.groupName.should.equal(ctx.subscription.teamName)
|
||||
@@ -514,28 +521,6 @@ describe('SubscriptionGroupController', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should redirect to manually collected subscription error page when collection method is manual and has no additional license add-on', async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual =
|
||||
sinon
|
||||
.stub()
|
||||
.throws(
|
||||
new ctx.Errors.HasNoAdditionalLicenseWhenManuallyCollectedError()
|
||||
)
|
||||
|
||||
const res = {
|
||||
redirect: url => {
|
||||
url.should.equal(
|
||||
'/user/subscription/group/manually-collected-subscription'
|
||||
)
|
||||
resolve()
|
||||
},
|
||||
}
|
||||
|
||||
ctx.Controller.addSeatsToGroupSubscription(ctx.req, res)
|
||||
})
|
||||
})
|
||||
|
||||
it('should redirect to subscription page when there is a pending change', async function (ctx) {
|
||||
await new Promise(resolve => {
|
||||
ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges =
|
||||
|
||||
@@ -806,49 +806,6 @@ describe('SubscriptionGroupHandler', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual', function () {
|
||||
it('should throw if the subscription is manually collected and has no additional license add-on', async function () {
|
||||
await expect(
|
||||
this.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
|
||||
{
|
||||
isCollectionMethodManual: true,
|
||||
hasAddOn: sinon
|
||||
.stub()
|
||||
.withArgs('additional-license')
|
||||
.returns(false),
|
||||
}
|
||||
)
|
||||
).to.be.rejectedWith(
|
||||
'This subscription is being collected manually has no "additional-license" add-on'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not throw if the subscription is not manually collected and has no additional license add-on and ', async function () {
|
||||
await expect(
|
||||
this.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
|
||||
{
|
||||
isCollectionMethodManual: false,
|
||||
hasAddOn: sinon
|
||||
.stub()
|
||||
.withArgs('additional-license')
|
||||
.returns(false),
|
||||
}
|
||||
)
|
||||
).to.not.be.rejected
|
||||
})
|
||||
|
||||
it('should not throw if the subscription is not manually collected and has additional license add-on', async function () {
|
||||
await expect(
|
||||
this.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
|
||||
{
|
||||
isCollectionMethodManual: true,
|
||||
hasAddOn: sinon.stub().withArgs('additional-license').returns(true),
|
||||
}
|
||||
)
|
||||
).to.not.be.rejected
|
||||
})
|
||||
})
|
||||
|
||||
describe('getGroupPlanUpgradePreview', function () {
|
||||
it('should generate preview for subscription upgrade', async function () {
|
||||
const result = await this.Handler.promises.getGroupPlanUpgradePreview(
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('UserMembershipHandler', function () {
|
||||
|
||||
this.UserMembershipViewModel = {
|
||||
promises: {
|
||||
buildAsync: sinon.stub().resolves([{ _id: 'mock-member-id' }]),
|
||||
buildAsync: sinon.stub().resolves({ _id: 'mock-member-id' }),
|
||||
},
|
||||
build: sinon.stub().returns(this.newUser),
|
||||
}
|
||||
@@ -118,26 +118,26 @@ describe('UserMembershipHandler', function () {
|
||||
this.subscription,
|
||||
EntityConfigs.group
|
||||
)
|
||||
const expectedCallcount =
|
||||
this.subscription.member_ids.length +
|
||||
this.subscription.invited_emails.length +
|
||||
this.subscription.teamInvites.length
|
||||
expect(
|
||||
this.UserMembershipViewModel.promises.buildAsync
|
||||
).to.be.calledOnceWith(
|
||||
this.subscription.invited_emails.concat(
|
||||
this.subscription.teamInvites[0].email,
|
||||
this.subscription.member_ids
|
||||
)
|
||||
)
|
||||
this.UserMembershipViewModel.promises.buildAsync.callCount
|
||||
).to.equal(expectedCallcount)
|
||||
})
|
||||
})
|
||||
|
||||
describe('group managers', function () {
|
||||
describe('group mamagers', function () {
|
||||
it('build view model for all managers', async function () {
|
||||
await this.UserMembershipHandler.promises.getUsers(
|
||||
this.subscription,
|
||||
EntityConfigs.groupManagers
|
||||
)
|
||||
const expectedCallcount = this.subscription.manager_ids.length
|
||||
expect(
|
||||
this.UserMembershipViewModel.promises.buildAsync
|
||||
).to.be.calledOnceWith(this.subscription.manager_ids)
|
||||
this.UserMembershipViewModel.promises.buildAsync.callCount
|
||||
).to.equal(expectedCallcount)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -147,9 +147,11 @@ describe('UserMembershipHandler', function () {
|
||||
this.institution,
|
||||
EntityConfigs.institution
|
||||
)
|
||||
|
||||
const expectedCallcount = this.institution.managerIds.length
|
||||
expect(
|
||||
this.UserMembershipViewModel.promises.buildAsync
|
||||
).to.be.calledOnceWith(this.institution.managerIds)
|
||||
this.UserMembershipViewModel.promises.buildAsync.callCount
|
||||
).to.equal(expectedCallcount)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -25,7 +25,7 @@ const {
|
||||
|
||||
describe('UserMembershipViewModel', function () {
|
||||
beforeEach(function () {
|
||||
this.UserGetter = { getUsers: sinon.stub() }
|
||||
this.UserGetter = { getUser: sinon.stub() }
|
||||
this.UserMembershipViewModel = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'mongodb-legacy': { ObjectId },
|
||||
@@ -87,10 +87,9 @@ describe('UserMembershipViewModel', function () {
|
||||
})
|
||||
|
||||
it('build email', function (done) {
|
||||
this.UserGetter.getUsers.yields(null, [])
|
||||
return this.UserMembershipViewModel.buildAsync(
|
||||
[this.email],
|
||||
(error, [viewModel]) => {
|
||||
this.email,
|
||||
(error, viewModel) => {
|
||||
assertCalledWith(this.UserMembershipViewModel.build, this.email)
|
||||
return done()
|
||||
}
|
||||
@@ -98,10 +97,9 @@ describe('UserMembershipViewModel', function () {
|
||||
})
|
||||
|
||||
it('build user', function (done) {
|
||||
this.UserGetter.getUsers.yields(null, [])
|
||||
return this.UserMembershipViewModel.buildAsync(
|
||||
[this.user],
|
||||
(error, [viewModel]) => {
|
||||
this.user,
|
||||
(error, viewModel) => {
|
||||
assertCalledWith(this.UserMembershipViewModel.build, this.user)
|
||||
return done()
|
||||
}
|
||||
@@ -109,34 +107,30 @@ describe('UserMembershipViewModel', function () {
|
||||
})
|
||||
|
||||
it('build user id', function (done) {
|
||||
const user = {
|
||||
...this.user,
|
||||
_id: new ObjectId(),
|
||||
}
|
||||
this.UserGetter.getUsers.yields(null, [user])
|
||||
this.UserGetter.getUser.yields(null, this.user)
|
||||
return this.UserMembershipViewModel.buildAsync(
|
||||
[user._id],
|
||||
(error, [viewModel]) => {
|
||||
new ObjectId(),
|
||||
(error, viewModel) => {
|
||||
expect(error).not.to.exist
|
||||
assertNotCalled(this.UserMembershipViewModel.build)
|
||||
expect(viewModel._id.toString()).to.equal(user._id.toString())
|
||||
expect(viewModel.email).to.equal(user.email)
|
||||
expect(viewModel.first_name).to.equal(user.first_name)
|
||||
expect(viewModel._id).to.equal(this.user._id)
|
||||
expect(viewModel.email).to.equal(this.user.email)
|
||||
expect(viewModel.first_name).to.equal(this.user.first_name)
|
||||
expect(viewModel.invite).to.equal(false)
|
||||
expect(viewModel.email).to.exist
|
||||
expect(viewModel.enrollment).to.exist
|
||||
expect(viewModel.enrollment).to.deep.equal(user.enrollment)
|
||||
expect(viewModel.enrollment).to.deep.equal(this.user.enrollment)
|
||||
return done()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('build user id with error', function (done) {
|
||||
this.UserGetter.getUsers.yields(new Error('nope'), [])
|
||||
this.UserGetter.getUser.yields(new Error('nope'))
|
||||
const userId = new ObjectId()
|
||||
return this.UserMembershipViewModel.buildAsync(
|
||||
[userId],
|
||||
(error, [viewModel]) => {
|
||||
userId,
|
||||
(error, viewModel) => {
|
||||
expect(error).not.to.exist
|
||||
assertNotCalled(this.UserMembershipViewModel.build)
|
||||
expect(viewModel._id).to.equal(userId.toString())
|
||||
|
||||
Reference in New Issue
Block a user