diff --git a/services/web/app/src/Features/Publishers/PublishersGetter.js b/services/web/app/src/Features/Publishers/PublishersGetter.js
index a25a5f1030..56b4482c84 100644
--- a/services/web/app/src/Features/Publishers/PublishersGetter.js
+++ b/services/web/app/src/Features/Publishers/PublishersGetter.js
@@ -1,35 +1,45 @@
-/* eslint-disable
- camelcase,
- n/handle-callback-err,
- max-len,
- no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-let PublishersGetter
+const Settings = require('@overleaf/settings')
+const logger = require('@overleaf/logger')
+const fetch = require('node-fetch')
+const { callbackify } = require('../../util/promises')
const UserMembershipsHandler = require('../UserMembership/UserMembershipsHandler')
const UserMembershipEntityConfigs = require('../UserMembership/UserMembershipEntityConfigs')
-const { promisifyAll } = require('../../util/promises')
-const logger = require('@overleaf/logger')
-const _ = require('underscore')
-module.exports = PublishersGetter = {
- getManagedPublishers(user_id, callback) {
- if (callback == null) {
- callback = function () {}
- }
- return UserMembershipsHandler.getEntitiesByUser(
- UserMembershipEntityConfigs.publisher,
- user_id,
- (error, managedPublishers) => callback(error, managedPublishers)
- )
- },
+async function getManagedPublishers(userId) {
+ return await UserMembershipsHandler.promises.getEntitiesByUser(
+ UserMembershipEntityConfigs.publisher,
+ userId
+ )
}
-module.exports.promises = promisifyAll(PublishersGetter)
+async function fetchV1Data(publisher) {
+ const url = `${Settings.apis.v1.url}/api/v2/brands/${publisher.slug}`
+ const authorization = `Basic ${Buffer.from(
+ Settings.apis.v1.user + ':' + Settings.apis.v1.pass
+ ).toString('base64')}`
+ try {
+ const response = await fetch(url, {
+ headers: {
+ Authorization: authorization,
+ },
+ signal: AbortSignal.timeout(Settings.apis.v1.timeout),
+ })
+ const data = await response.json()
+
+ publisher.name = data?.name
+ publisher.partner = data?.partner
+ } catch (error) {
+ logger.err(
+ { model: 'Publisher', slug: publisher.slug, error },
+ '[fetchV1DataError]'
+ )
+ }
+}
+
+module.exports = {
+ getManagedPublishers: callbackify(getManagedPublishers),
+ promises: {
+ getManagedPublishers,
+ fetchV1Data,
+ },
+}
diff --git a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js
index 325bb25fc7..ffd16d2392 100644
--- a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js
+++ b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js
@@ -191,6 +191,10 @@ async function buildUsersSubscriptionViewModel(user) {
await Promise.all(
managedInstitutions.map(InstitutionsManager.promises.fetchV1Data)
)
+ managedPublishers = managedPublishers.map(serializeMongooseObject)
+ await Promise.all(
+ managedPublishers.map(PublishersGetter.promises.fetchV1Data)
+ )
if (plan != null) {
personalSubscription.plan = plan
diff --git a/services/web/app/views/subscriptions/dashboard-react.pug b/services/web/app/views/subscriptions/dashboard-react.pug
index 61513faf09..08fb6479db 100644
--- a/services/web/app/views/subscriptions/dashboard-react.pug
+++ b/services/web/app/views/subscriptions/dashboard-react.pug
@@ -7,8 +7,9 @@ block head-scripts
script(type="text/javascript", nonce=scriptNonce, src="https://js.recurly.com/v4/recurly.js")
block append meta
- meta(name="ol-managedInstitutions", data-type="json", content=managedInstitutions)
meta(name="ol-managedGroupSubscriptions", data-type="json" content=managedGroupSubscriptions)
+ 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)
meta(name="ol-currentInstitutionsWithLicence", data-type="json" content=currentInstitutionsWithLicence)
meta(name="ol-plans", data-type="json" content=plans)
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 8206d91c5e..5aee55b2e6 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -458,6 +458,7 @@
"manage_labs_program_membership": "",
"manage_members": "",
"manage_newsletter": "",
+ "manage_publisher_managers": "",
"manage_sessions": "",
"managers_management": "",
"math_display": "",
@@ -904,6 +905,7 @@
"year": "",
"you_are_a_manager_and_member_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "",
"you_are_a_manager_of_commons_at_institution_x": "",
+ "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_can_now_log_in_sso": "",
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/managed_institution.tsx b/services/web/frontend/js/features/subscription/components/dashboard/managed-institution.tsx
similarity index 100%
rename from services/web/frontend/js/features/subscription/components/dashboard/managed_institution.tsx
rename to services/web/frontend/js/features/subscription/components/dashboard/managed-institution.tsx
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/managed-institutions.tsx b/services/web/frontend/js/features/subscription/components/dashboard/managed-institutions.tsx
index 3ff725fc93..8e6279dbfc 100644
--- a/services/web/frontend/js/features/subscription/components/dashboard/managed-institutions.tsx
+++ b/services/web/frontend/js/features/subscription/components/dashboard/managed-institutions.tsx
@@ -1,5 +1,5 @@
import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
-import ManagedInstitution from './managed_institution'
+import ManagedInstitution from './managed-institution'
export type Institution = {
v1Id: number
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/managed-publisher.tsx b/services/web/frontend/js/features/subscription/components/dashboard/managed-publisher.tsx
new file mode 100644
index 0000000000..5e05c877e5
--- /dev/null
+++ b/services/web/frontend/js/features/subscription/components/dashboard/managed-publisher.tsx
@@ -0,0 +1,35 @@
+import { Trans, useTranslation } from 'react-i18next'
+import { Publisher } from './managed-publishers'
+
+type ManagedPublisherProps = {
+ publisher: Publisher
+}
+
+export default function ManagedPublisher({ publisher }: ManagedPublisherProps) {
+ const { t } = useTranslation()
+
+ return (
+
+ )
+}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/managed-publishers.tsx b/services/web/frontend/js/features/subscription/components/dashboard/managed-publishers.tsx
new file mode 100644
index 0000000000..a27e8c9456
--- /dev/null
+++ b/services/web/frontend/js/features/subscription/components/dashboard/managed-publishers.tsx
@@ -0,0 +1,28 @@
+import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
+import ManagedPublisher from './managed-publisher'
+
+export type Publisher = {
+ slug: string
+ managerIds: string[]
+ name: string
+ partner: string
+}
+
+export default function ManagedPublishers() {
+ const { managedPublishers } = useSubscriptionDashboardContext()
+
+ if (!managedPublishers) {
+ return null
+ }
+
+ return (
+ <>
+ {managedPublishers.map(publisher => (
+
+ ))}
+ >
+ )
+}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx b/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx
index c3f8b562d1..bb6994731d 100644
--- a/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx
+++ b/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx
@@ -1,6 +1,7 @@
import { useTranslation } from 'react-i18next'
import InstitutionMemberships from './institution-memberships'
import FreePlan from './free-plan'
+import ManagedPublishers from './managed-publishers'
import PersonalSubscription from './personal-subscription'
import ManagedGroupSubscriptions from './managed-group-subscriptions'
import ManagedInstitutions from './managed-institutions'
@@ -22,6 +23,7 @@ function SubscriptionDashboard() {
+
{!hasDisplayedSubscription && }
diff --git a/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx b/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx
index b36e9843b1..213bda972f 100644
--- a/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx
+++ b/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx
@@ -15,8 +15,9 @@ import {
Plan,
PriceForDisplayData,
} from '../../../../../types/subscription/plan'
-import { Institution as ManagedInstitution } from '../components/dashboard/managed-institutions'
import { Institution } from '../../../../../types/institution'
+import { Institution as ManagedInstitution } from '../components/dashboard/managed-institutions'
+import { Publisher as ManagedPublisher } from '../components/dashboard/managed-publishers'
import getMeta from '../../../utils/meta'
import {
loadDisplayPriceWithTaxPromise,
@@ -40,6 +41,7 @@ type SubscriptionDashboardContextValue = {
institutionMemberships?: Institution[]
managedGroupSubscriptions: ManagedGroupSubscription[]
managedInstitutions: ManagedInstitution[]
+ managedPublishers: ManagedPublisher[]
updateManagedInstitution: (institution: ManagedInstitution) => void
modalIdShown?: SubscriptionDashModalIds
personalSubscription?: Subscription
@@ -106,13 +108,15 @@ export function SubscriptionDashboardProvider({
const [managedInstitutions, setManagedInstitutions] = useState<
ManagedInstitution[]
>(getMeta('ol-managedInstitutions'))
+ const managedPublishers = getMeta('ol-managedPublishers')
const recurlyApiKey = getMeta('ol-recurlyApiKey')
const hasDisplayedSubscription =
institutionMemberships?.length > 0 ||
personalSubscription ||
managedGroupSubscriptions?.length > 0 ||
- managedInstitutions?.length > 0
+ managedInstitutions?.length > 0 ||
+ managedPublishers?.length > 0
useEffect(() => {
if (!isRecurlyLoaded()) {
@@ -224,6 +228,7 @@ export function SubscriptionDashboardProvider({
institutionMemberships,
managedGroupSubscriptions,
managedInstitutions,
+ managedPublishers,
updateManagedInstitution,
modalIdShown,
personalSubscription,
@@ -255,6 +260,7 @@ export function SubscriptionDashboardProvider({
institutionMemberships,
managedGroupSubscriptions,
managedInstitutions,
+ managedPublishers,
updateManagedInstitution,
modalIdShown,
personalSubscription,
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index f07c1e4afd..dd92a4c0f9 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -892,6 +892,7 @@
"manage_labs_program_membership": "Manage Labs Program Membership",
"manage_members": "Manage members",
"manage_newsletter": "Manage Your Newsletter Preferences",
+ "manage_publisher_managers": "Manage publisher managers",
"manage_sessions": "Manage Your Sessions",
"manage_subscription": "Manage Subscription",
"managers_cannot_remove_admin": "Admins cannot be removed",
@@ -1160,6 +1161,7 @@
"public": "Public",
"publish": "Publish",
"publish_as_template": "Manage Template",
+ "publisher_account": "Publisher Account",
"publishing": "Publishing",
"pull_github_changes_into_sharelatex": "Pull GitHub changes into __appName__",
"push_sharelatex_changes_to_github": "Push __appName__ changes to GitHub",
@@ -1682,6 +1684,7 @@
"you_and_collaborators_get_access_to_info": "These features are available to you and your collaborators (other Overleaf users that you invite to your projects).",
"you_are_a_manager_and_member_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "You are a <1>manager1> and <1>member1> of the <0>__planName__0> group subscription <1>__groupName__1> administered by <1>__adminEmail__1>",
"you_are_a_manager_of_commons_at_institution_x": "You are a <0>manager0> of the Overleaf Commons subscription at <0>__institutionName__0>",
+ "you_are_a_manager_of_publisher_x": "You are a <0>manager0> of <0>__publisherName__0>",
"you_are_a_manager_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "You are a <1>manager1> of the <0>__planName__0> group subscription <1>__groupName__1> administered by <1>__adminEmail__1>",
"you_are_about_to_upgrade": "You are about to upgrade to the __planName__",
"you_are_on_x_plan_as_a_confirmed_member_of_institution_y": "You are on our <0>__planName__0> plan as a <1>confirmed member1> of <1>__institutionName__1>",
diff --git a/services/web/test/frontend/features/subscription/components/dashboard/managed-publishers.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/managed-publishers.test.tsx
new file mode 100644
index 0000000000..fe746c8988
--- /dev/null
+++ b/services/web/test/frontend/features/subscription/components/dashboard/managed-publishers.test.tsx
@@ -0,0 +1,85 @@
+import { expect } from 'chai'
+import { render, screen } from '@testing-library/react'
+import { SubscriptionDashboardProvider } from '../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
+import fetchMock from 'fetch-mock'
+import ManagedPublishers, {
+ Publisher,
+} from '../../../../../../frontend/js/features/subscription/components/dashboard/managed-publishers'
+
+const userId = 'fff999fff999'
+const publisher1 = {
+ slug: 'pub-1',
+ managerIds: [],
+ name: 'Pub 1',
+ partner: 'p1',
+}
+const publisher2 = {
+ slug: 'pub-2',
+ managerIds: [],
+ name: 'Pub 2',
+ partner: 'p2',
+}
+const managedPublishers: Publisher[] = [publisher1, publisher2]
+
+describe('', function () {
+ beforeEach(function () {
+ window.metaAttributesCache = new Map()
+ window.metaAttributesCache.set('ol-managedPublishers', managedPublishers)
+ window.user_id = userId
+ })
+
+ afterEach(function () {
+ window.metaAttributesCache = new Map()
+ delete window.user_id
+ fetchMock.reset()
+ })
+
+ it('renders all managed publishers', function () {
+ render(
+
+
+
+ )
+
+ 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 of Pub 1')
+ expect(elements[1].textContent).to.equal('You are a manager of Pub 2')
+
+ const viewHubLinks = screen.getAllByText('View hub')
+ expect(viewHubLinks.length).to.equal(2)
+ expect(viewHubLinks[0].getAttribute('href')).to.equal(
+ '/publishers/pub-1/hub'
+ )
+ expect(viewHubLinks[1].getAttribute('href')).to.equal(
+ '/publishers/pub-2/hub'
+ )
+
+ const manageGroupManagersLinks = screen.getAllByText(
+ 'Manage publisher managers'
+ )
+ expect(manageGroupManagersLinks.length).to.equal(2)
+ expect(manageGroupManagersLinks[0].getAttribute('href')).to.equal(
+ '/manage/publishers/pub-1/managers'
+ )
+ expect(manageGroupManagersLinks[1].getAttribute('href')).to.equal(
+ '/manage/publishers/pub-2/managers'
+ )
+ })
+
+ it('renders nothing when there are no publishers', function () {
+ window.metaAttributesCache.set('ol-managedPublishers', undefined)
+
+ render(
+
+
+
+ )
+ const elements = screen.queryAllByText('You are a', {
+ exact: false,
+ })
+ expect(elements.length).to.equal(0)
+ })
+})
diff --git a/services/web/test/unit/src/Publishers/PublishersGetterTests.js b/services/web/test/unit/src/Publishers/PublishersGetterTests.js
index 65939339bf..bad46039de 100644
--- a/services/web/test/unit/src/Publishers/PublishersGetterTests.js
+++ b/services/web/test/unit/src/Publishers/PublishersGetterTests.js
@@ -1,19 +1,6 @@
-/* eslint-disable
- n/handle-callback-err,
- max-len,
- no-return-assign,
- no-unused-vars,
-*/
-// TODO: This file was created by bulk-decaffeinate.
-// Fix any style issues and re-enable lint.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
const SandboxedModule = require('sandboxed-module')
-const { expect } = require('chai')
const sinon = require('sinon')
+const { expect } = require('chai')
const modulePath = require('path').join(
__dirname,
'../../../../app/src/Features/Publishers/PublishersGetter.js'
@@ -27,38 +14,41 @@ describe('PublishersGetter', function () {
fetchV1Data: sinon.stub(),
}
+ this.UserMembershipsHandler = {
+ promises: {
+ getEntitiesByUser: sinon.stub().resolves([this.publisher]),
+ },
+ }
+ this.UserMembershipEntityConfigs = {
+ publisher: {
+ modelName: 'Publisher',
+ canCreate: true,
+ fields: {
+ primaryKey: 'slug',
+ },
+ },
+ }
+
this.PublishersGetter = SandboxedModule.require(modulePath, {
requires: {
'../User/UserGetter': this.UserGetter,
- '../UserMembership/UserMembershipsHandler':
- (this.UserMembershipsHandler = {
- getEntitiesByUser: sinon
- .stub()
- .callsArgWith(2, null, [this.publisher]),
- }),
+ '../UserMembership/UserMembershipsHandler': this.UserMembershipsHandler,
'../UserMembership/UserMembershipEntityConfigs':
- (this.UserMembershipEntityConfigs = {
- publisher: {
- modelName: 'Publisher',
- canCreate: true,
- fields: {
- primaryKey: 'slug',
- },
- },
- }),
+ this.UserMembershipEntityConfigs,
},
})
- return (this.userId = '12345abcde')
+ this.userId = '12345abcde'
})
describe('getManagedPublishers', function () {
it('fetches v1 data before returning publisher list', function (done) {
- return this.PublishersGetter.getManagedPublishers(
+ this.PublishersGetter.getManagedPublishers(
this.userId,
(error, publishers) => {
+ expect(error).to.be.null
publishers.length.should.equal(1)
- return done()
+ done()
}
)
})