From d6e9508aed6e87b7bffc7b25f97e718b39b3faa4 Mon Sep 17 00:00:00 2001 From: Alexandre Bourdin Date: Wed, 22 Feb 2023 12:50:24 +0100 Subject: [PATCH] [web] Migrate managed publishers to React dash (#11749) * Migrate managed publishers to React dash * Decaf cleanup + async/await PublishersGetter * Continue migration of managed publishers to react dash * Fix linting * Add tests * Decaf cleanup PublishersGetterTests * Update PublishersGetter tests * Rename component files to kebab-case GitOrigin-RevId: cb1fe14d120457c965a9d23a8ddb2c2c92e1d5da --- .../Features/Publishers/PublishersGetter.js | 70 ++++++++------- .../SubscriptionViewModelBuilder.js | 4 + .../views/subscriptions/dashboard-react.pug | 3 +- .../web/frontend/extracted-translations.json | 2 + ...nstitution.tsx => managed-institution.tsx} | 0 .../dashboard/managed-institutions.tsx | 2 +- .../dashboard/managed-publisher.tsx | 35 ++++++++ .../dashboard/managed-publishers.tsx | 28 ++++++ .../dashboard/subscription-dashboard.tsx | 2 + .../subscription-dashboard-context.tsx | 10 ++- services/web/locales/en.json | 3 + .../dashboard/managed-publishers.test.tsx | 85 +++++++++++++++++++ .../src/Publishers/PublishersGetterTests.js | 54 +++++------- 13 files changed, 232 insertions(+), 66 deletions(-) rename services/web/frontend/js/features/subscription/components/dashboard/{managed_institution.tsx => managed-institution.tsx} (100%) create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/managed-publisher.tsx create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/managed-publishers.tsx create mode 100644 services/web/test/frontend/features/subscription/components/dashboard/managed-publishers.test.tsx 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 ( +
+

+ ]} // eslint-disable-line react/jsx-key + values={{ + publisherName: publisher.name || '', + }} + /> +

+

+ + {t('view_hub')} + +

+

+ + {t('manage_publisher_managers')} + +

+
+
+ ) +} 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>manager and <1>member of the <0>__planName__ group subscription <1>__groupName__ administered by <1>__adminEmail__", "you_are_a_manager_of_commons_at_institution_x": "You are a <0>manager of the Overleaf Commons subscription at <0>__institutionName__", + "you_are_a_manager_of_publisher_x": "You are a <0>manager of <0>__publisherName__", "you_are_a_manager_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "You are a <1>manager of the <0>__planName__ group subscription <1>__groupName__ administered by <1>__adminEmail__", "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__ plan as a <1>confirmed member of <1>__institutionName__", 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() } ) })