From 7608d37c0a7a6b76b5329a4e54cf71fb0d3d371c Mon Sep 17 00:00:00 2001 From: Alexandre Bourdin Date: Mon, 26 Sep 2022 10:57:29 +0200 Subject: [PATCH] Merge pull request #9622 from overleaf/mf-add-portal-templates-2 Add institution templates to the react version of the new project dropdown GitOrigin-RevId: 32bf0b1b559ea3da744430902cc016e7c2a918d9 --- .../Features/Project/ProjectListController.js | 46 ++++- services/web/app/views/project/list-react.pug | 1 + .../web/frontend/extracted-translations.json | 1 + .../components/new-project-button.tsx | 18 ++ .../components/new-project-button.test.tsx | 165 ++++++++++++------ services/web/types/portal-template.ts | 4 + 6 files changed, 175 insertions(+), 60 deletions(-) create mode 100644 services/web/types/portal-template.ts diff --git a/services/web/app/src/Features/Project/ProjectListController.js b/services/web/app/src/Features/Project/ProjectListController.js index cf86717932..9c35778280 100644 --- a/services/web/app/src/Features/Project/ProjectListController.js +++ b/services/web/app/src/Features/Project/ProjectListController.js @@ -1,4 +1,5 @@ const _ = require('lodash') +const Settings = require('@overleaf/settings') const ProjectHelper = require('./ProjectHelper') const ProjectGetter = require('./ProjectGetter') const PrivilegeLevels = require('../Authorization/PrivilegeLevels') @@ -19,6 +20,17 @@ const SplitTestHandler = require('../SplitTests/SplitTestHandler') const UserPrimaryEmailCheckHandler = require('../User/UserPrimaryEmailCheckHandler') const UserController = require('../User/UserController') +/** @typedef {import("./types").GetProjectsRequest} GetProjectsRequest */ +/** @typedef {import("./types").GetProjectsResponse} GetProjectsResponse */ +/** @typedef {import("../../../../types/project/dashboard/api").Project} Project */ +/** @typedef {import("../../../../types/project/dashboard/api").Filters} Filters */ +/** @typedef {import("../../../../types/project/dashboard/api").Page} Page */ +/** @typedef {import("../../../../types/project/dashboard/api").Sort} Sort */ +/** @typedef {import("./types").AllUsersProjects} AllUsersProjects */ +/** @typedef {import("./types").MongoProject} MongoProject */ + +/** @typedef {import("../Tags/types").Tag} Tag */ + const _ssoAvailable = (affiliation, session, linkedInstitutionIds) => { if (!affiliation.institution) return false @@ -38,16 +50,29 @@ const _ssoAvailable = (affiliation, session, linkedInstitutionIds) => { return false } -/** @typedef {import("./types").GetProjectsRequest} GetProjectsRequest */ -/** @typedef {import("./types").GetProjectsResponse} GetProjectsResponse */ -/** @typedef {import("../../../../types/project/dashboard/api").Project} Project */ -/** @typedef {import("../../../../types/project/dashboard/api").Filters} Filters */ -/** @typedef {import("../../../../types/project/dashboard/api").Page} Page */ -/** @typedef {import("../../../../types/project/dashboard/api").Sort} Sort */ -/** @typedef {import("./types").AllUsersProjects} AllUsersProjects */ -/** @typedef {import("./types").MongoProject} MongoProject */ +const _buildPortalTemplatesList = affiliations => { + if (affiliations == null) { + affiliations = [] + } -/** @typedef {import("../Tags/types").Tag} Tag */ + const portalTemplates = [] + const uniqueAffiliations = _.uniqBy(affiliations, 'institution.id') + for (const aff of uniqueAffiliations) { + const hasSlug = aff.portal?.slug + const hasTemplates = aff.portal?.templates_count > 0 + + if (hasSlug && hasTemplates) { + const portalPath = aff.institution.isUniversity ? '/edu/' : '/org/' + const portalTemplateURL = Settings.siteUrl + portalPath + aff.portal?.slug + + portalTemplates.push({ + name: aff.institution.name, + url: portalTemplateURL, + }) + } + } + return portalTemplates +} /** * @param {import("express").Request} req @@ -158,6 +183,8 @@ async function projectListReactPage(req, res, next) { return result }) + const portalTemplates = _buildPortalTemplatesList(userAffiliations) + const { allInReconfirmNotificationPeriods } = userEmailsData const notifications = @@ -258,6 +285,7 @@ async function projectListReactPage(req, res, next) { allInReconfirmNotificationPeriods, survey, tags, + portalTemplates, }) } diff --git a/services/web/app/views/project/list-react.pug b/services/web/app/views/project/list-react.pug index 9ee7e4004d..460e71f626 100644 --- a/services/web/app/views/project/list-react.pug +++ b/services/web/app/views/project/list-react.pug @@ -16,6 +16,7 @@ block append meta meta(name="ol-reconfirmedViaSAML" content=reconfirmedViaSAML) meta(name="ol-survey" data-type="json" content=survey) meta(name="ol-tags" data-type="json" content=tags) + meta(name="ol-portalTemplates" data-type="json" content=portalTemplates) block content main.content.content-alt.project-list-react#project-list-root diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 029a52849e..fe60b1cd83 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -292,6 +292,7 @@ "importing_and_merging_changes_in_github": "", "in_order_to_match_institutional_metadata_2": "", "in_order_to_match_institutional_metadata_associated": "", + "institution": "", "institution_acct_successfully_linked_2": "", "institution_and_role": "", "institutional_leavers_survey_notification": "", diff --git a/services/web/frontend/js/features/project-list/components/new-project-button.tsx b/services/web/frontend/js/features/project-list/components/new-project-button.tsx index 2fbdc3551b..984d7a5eca 100644 --- a/services/web/frontend/js/features/project-list/components/new-project-button.tsx +++ b/services/web/frontend/js/features/project-list/components/new-project-button.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { Dropdown, MenuItem } from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { ExposedSettings } from '../../../../../types/exposed-settings' +import type { PortalTemplate } from '../../../../../types/portal-template' import ControlledDropdown from '../../../shared/components/controlled-dropdown' import getMeta from '../../../utils/meta' import NewProjectButtonModal, { @@ -24,6 +25,7 @@ function NewProjectButton({ const { templateLinks } = getMeta('ol-ExposedSettings') as ExposedSettings const [modal, setModal] = useState>(null) + const portalTemplates = getMeta('ol-portalTemplates') as PortalTemplate[] return ( <> @@ -48,6 +50,22 @@ function NewProjectButton({ setModal('import_from_github')}> {t('import_from_github')} + {portalTemplates?.length > 0 ? ( + <> + + + {`${t('institution')} ${t('templates')}`} + + {portalTemplates.map((portalTemplate, index) => ( + + {portalTemplate.name} + + ))} + + ) : null} {t('templates')} {templateLinks.map((templateLink, index) => ( diff --git a/services/web/test/frontend/features/project-list/components/new-project-button.test.tsx b/services/web/test/frontend/features/project-list/components/new-project-button.test.tsx index d578320f0d..90bd66ecb4 100644 --- a/services/web/test/frontend/features/project-list/components/new-project-button.test.tsx +++ b/services/web/test/frontend/features/project-list/components/new-project-button.test.tsx @@ -3,70 +3,133 @@ import { expect } from 'chai' import NewProjectButton from '../../../../../frontend/js/features/project-list/components/new-project-button' describe('', function () { - beforeEach(function () { - window.metaAttributesCache.set('ol-ExposedSettings', { - templateLinks: [ - { - name: 'Academic Journal', - url: '/gallery/tagged/academic-journal', - }, - { - name: 'View All', - url: '/latex/templates', - }, - ], + describe('for every user (affiliated and non-affiliated)', function () { + beforeEach(function () { + window.metaAttributesCache.set('ol-ExposedSettings', { + templateLinks: [ + { + name: 'Academic Journal', + url: '/gallery/tagged/academic-journal', + }, + { + name: 'View All', + url: '/latex/templates', + }, + ], + }) + + render() + + const newProjectButton = screen.getByRole('button', { + name: 'New Project', + }) + fireEvent.click(newProjectButton) }) - render() - - const newProjectButton = screen.getByRole('button', { - name: 'New Project', + afterEach(function () { + window.metaAttributesCache = new Map() + }) + + it('shows the correct dropdown menu', function () { + // static menu + screen.getByText('Blank Project') + screen.getByText('Example Project') + screen.getByText('Upload Project') + screen.getByText('Import from GitHub') + + // static text + screen.getByText('Templates') + + // dynamic menu based on templateLinks + screen.getByText('Academic Journal') + screen.getByText('View All') + }) + + it('open new project modal when clicking at Blank Project', function () { + fireEvent.click(screen.getByRole('menuitem', { name: 'Blank Project' })) + + screen.getByPlaceholderText('Project Name') + }) + + it('open new project modal when clicking at Example Project', function () { + fireEvent.click(screen.getByRole('menuitem', { name: 'Example Project' })) + + screen.getByPlaceholderText('Project Name') + }) + + it('close the new project modal when clicking at the top right "x" button', function () { + fireEvent.click(screen.getByRole('menuitem', { name: 'Blank Project' })) + fireEvent.click(screen.getByRole('button', { name: 'Close' })) + + expect(screen.queryByRole('dialog')).to.be.null + }) + + it('close the new project modal when clicking at the Cancel button', function () { + fireEvent.click(screen.getByRole('menuitem', { name: 'Blank Project' })) + fireEvent.click(screen.getByRole('button', { name: 'Cancel' })) + + expect(screen.queryByRole('dialog')).to.be.null }) - fireEvent.click(newProjectButton) }) - afterEach(function () { - window.metaAttributesCache = new Map() - }) + describe('for affiliated user with custom templates', function () { + beforeEach(function () { + window.metaAttributesCache.set('ol-ExposedSettings', { + templateLinks: [ + { + name: 'Academic Journal', + url: '/gallery/tagged/academic-journal', + }, + { + name: 'View All', + url: '/latex/templates', + }, + ], + }) - it('opens a dropdown', function () { - // static menu - screen.getByText('Blank Project') - screen.getByText('Example Project') - screen.getByText('Upload Project') - screen.getByText('Import from GitHub') + window.metaAttributesCache.set('ol-portalTemplates', [ + { + name: 'Affiliation 1', + url: '/edu/test-new-template', + }, + ]) + }) - // static text - screen.getByText('Templates') + afterEach(function () { + window.metaAttributesCache = new Map() + }) - // dynamic menu based on templateLinks - screen.getByText('Academic Journal') - screen.getByText('View All') - }) + it('shows the correct dropdown menu', function () { + render() - it('open new project modal when clicking at Blank Project', function () { - fireEvent.click(screen.getByRole('menuitem', { name: 'Blank Project' })) + const newProjectButton = screen.getByRole('button', { + name: 'New Project', + }) - screen.getByPlaceholderText('Project Name') - }) + fireEvent.click(newProjectButton) + // static menu + screen.getByText('Blank Project') + screen.getByText('Example Project') + screen.getByText('Upload Project') + screen.getByText('Import from GitHub') - it('open new project modal when clicking at Example Project', function () { - fireEvent.click(screen.getByRole('menuitem', { name: 'Example Project' })) + // static text for institution templates + screen.getByText('Institution Templates') - screen.getByPlaceholderText('Project Name') - }) + // dynamic menu based on portalTemplates + const affiliationTemplate = screen.getByRole('menuitem', { + name: 'Affiliation 1', + }) + expect(affiliationTemplate.getAttribute('href')).to.equal( + '/edu/test-new-template#templates' + ) - it('close the new project modal when clicking at the top right "x" button', function () { - fireEvent.click(screen.getByRole('menuitem', { name: 'Blank Project' })) - fireEvent.click(screen.getByRole('button', { name: 'Close' })) + // static text + screen.getByText('Templates') - expect(screen.queryByRole('dialog')).to.be.null - }) - - it('close the new project modal when clicking at the Cancel button', function () { - fireEvent.click(screen.getByRole('menuitem', { name: 'Blank Project' })) - fireEvent.click(screen.getByRole('button', { name: 'Cancel' })) - - expect(screen.queryByRole('dialog')).to.be.null + // dynamic menu based on templateLinks + screen.getByText('Academic Journal') + screen.getByText('View All') + }) }) }) diff --git a/services/web/types/portal-template.ts b/services/web/types/portal-template.ts new file mode 100644 index 0000000000..facaaa1d92 --- /dev/null +++ b/services/web/types/portal-template.ts @@ -0,0 +1,4 @@ +export type PortalTemplate = { + name: string + url: string +}