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
This commit is contained in:
Alexandre Bourdin 2022-09-26 10:57:29 +02:00 committed by Copybot
parent c42cedbcdc
commit 7608d37c0a
6 changed files with 175 additions and 60 deletions

View file

@ -1,4 +1,5 @@
const _ = require('lodash') const _ = require('lodash')
const Settings = require('@overleaf/settings')
const ProjectHelper = require('./ProjectHelper') const ProjectHelper = require('./ProjectHelper')
const ProjectGetter = require('./ProjectGetter') const ProjectGetter = require('./ProjectGetter')
const PrivilegeLevels = require('../Authorization/PrivilegeLevels') const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
@ -19,6 +20,17 @@ const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const UserPrimaryEmailCheckHandler = require('../User/UserPrimaryEmailCheckHandler') const UserPrimaryEmailCheckHandler = require('../User/UserPrimaryEmailCheckHandler')
const UserController = require('../User/UserController') 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) => { const _ssoAvailable = (affiliation, session, linkedInstitutionIds) => {
if (!affiliation.institution) return false if (!affiliation.institution) return false
@ -38,16 +50,29 @@ const _ssoAvailable = (affiliation, session, linkedInstitutionIds) => {
return false return false
} }
/** @typedef {import("./types").GetProjectsRequest} GetProjectsRequest */ const _buildPortalTemplatesList = affiliations => {
/** @typedef {import("./types").GetProjectsResponse} GetProjectsResponse */ if (affiliations == null) {
/** @typedef {import("../../../../types/project/dashboard/api").Project} Project */ affiliations = []
/** @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 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 * @param {import("express").Request} req
@ -158,6 +183,8 @@ async function projectListReactPage(req, res, next) {
return result return result
}) })
const portalTemplates = _buildPortalTemplatesList(userAffiliations)
const { allInReconfirmNotificationPeriods } = userEmailsData const { allInReconfirmNotificationPeriods } = userEmailsData
const notifications = const notifications =
@ -258,6 +285,7 @@ async function projectListReactPage(req, res, next) {
allInReconfirmNotificationPeriods, allInReconfirmNotificationPeriods,
survey, survey,
tags, tags,
portalTemplates,
}) })
} }

View file

@ -16,6 +16,7 @@ block append meta
meta(name="ol-reconfirmedViaSAML" content=reconfirmedViaSAML) meta(name="ol-reconfirmedViaSAML" content=reconfirmedViaSAML)
meta(name="ol-survey" data-type="json" content=survey) meta(name="ol-survey" data-type="json" content=survey)
meta(name="ol-tags" data-type="json" content=tags) meta(name="ol-tags" data-type="json" content=tags)
meta(name="ol-portalTemplates" data-type="json" content=portalTemplates)
block content block content
main.content.content-alt.project-list-react#project-list-root main.content.content-alt.project-list-react#project-list-root

View file

@ -292,6 +292,7 @@
"importing_and_merging_changes_in_github": "", "importing_and_merging_changes_in_github": "",
"in_order_to_match_institutional_metadata_2": "", "in_order_to_match_institutional_metadata_2": "",
"in_order_to_match_institutional_metadata_associated": "", "in_order_to_match_institutional_metadata_associated": "",
"institution": "",
"institution_acct_successfully_linked_2": "", "institution_acct_successfully_linked_2": "",
"institution_and_role": "", "institution_and_role": "",
"institutional_leavers_survey_notification": "", "institutional_leavers_survey_notification": "",

View file

@ -2,6 +2,7 @@ import { useState } from 'react'
import { Dropdown, MenuItem } from 'react-bootstrap' import { Dropdown, MenuItem } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ExposedSettings } from '../../../../../types/exposed-settings' import { ExposedSettings } from '../../../../../types/exposed-settings'
import type { PortalTemplate } from '../../../../../types/portal-template'
import ControlledDropdown from '../../../shared/components/controlled-dropdown' import ControlledDropdown from '../../../shared/components/controlled-dropdown'
import getMeta from '../../../utils/meta' import getMeta from '../../../utils/meta'
import NewProjectButtonModal, { import NewProjectButtonModal, {
@ -24,6 +25,7 @@ function NewProjectButton({
const { templateLinks } = getMeta('ol-ExposedSettings') as ExposedSettings const { templateLinks } = getMeta('ol-ExposedSettings') as ExposedSettings
const [modal, setModal] = const [modal, setModal] =
useState<Nullable<NewProjectButtonModalVariant>>(null) useState<Nullable<NewProjectButtonModalVariant>>(null)
const portalTemplates = getMeta('ol-portalTemplates') as PortalTemplate[]
return ( return (
<> <>
@ -48,6 +50,22 @@ function NewProjectButton({
<MenuItem onClick={() => setModal('import_from_github')}> <MenuItem onClick={() => setModal('import_from_github')}>
{t('import_from_github')} {t('import_from_github')}
</MenuItem> </MenuItem>
{portalTemplates?.length > 0 ? (
<>
<MenuItem divider />
<MenuItem header>
{`${t('institution')} ${t('templates')}`}
</MenuItem>
{portalTemplates.map((portalTemplate, index) => (
<MenuItem
key={`portal-template-${index}`}
href={`${portalTemplate.url}#templates`}
>
{portalTemplate.name}
</MenuItem>
))}
</>
) : null}
<MenuItem divider /> <MenuItem divider />
<MenuItem header>{t('templates')}</MenuItem> <MenuItem header>{t('templates')}</MenuItem>
{templateLinks.map((templateLink, index) => ( {templateLinks.map((templateLink, index) => (

View file

@ -3,6 +3,7 @@ import { expect } from 'chai'
import NewProjectButton from '../../../../../frontend/js/features/project-list/components/new-project-button' import NewProjectButton from '../../../../../frontend/js/features/project-list/components/new-project-button'
describe('<NewProjectButton />', function () { describe('<NewProjectButton />', function () {
describe('for every user (affiliated and non-affiliated)', function () {
beforeEach(function () { beforeEach(function () {
window.metaAttributesCache.set('ol-ExposedSettings', { window.metaAttributesCache.set('ol-ExposedSettings', {
templateLinks: [ templateLinks: [
@ -29,7 +30,7 @@ describe('<NewProjectButton />', function () {
window.metaAttributesCache = new Map() window.metaAttributesCache = new Map()
}) })
it('opens a dropdown', function () { it('shows the correct dropdown menu', function () {
// static menu // static menu
screen.getByText('Blank Project') screen.getByText('Blank Project')
screen.getByText('Example Project') screen.getByText('Example Project')
@ -69,4 +70,66 @@ describe('<NewProjectButton />', function () {
expect(screen.queryByRole('dialog')).to.be.null expect(screen.queryByRole('dialog')).to.be.null
}) })
})
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',
},
],
})
window.metaAttributesCache.set('ol-portalTemplates', [
{
name: 'Affiliation 1',
url: '/edu/test-new-template',
},
])
})
afterEach(function () {
window.metaAttributesCache = new Map()
})
it('shows the correct dropdown menu', function () {
render(<NewProjectButton id="test" />)
const newProjectButton = screen.getByRole('button', {
name: 'New Project',
})
fireEvent.click(newProjectButton)
// static menu
screen.getByText('Blank Project')
screen.getByText('Example Project')
screen.getByText('Upload Project')
screen.getByText('Import from GitHub')
// static text for institution templates
screen.getByText('Institution Templates')
// dynamic menu based on portalTemplates
const affiliationTemplate = screen.getByRole('menuitem', {
name: 'Affiliation 1',
})
expect(affiliationTemplate.getAttribute('href')).to.equal(
'/edu/test-new-template#templates'
)
// static text
screen.getByText('Templates')
// dynamic menu based on templateLinks
screen.getByText('Academic Journal')
screen.getByText('View All')
})
})
}) })

View file

@ -0,0 +1,4 @@
export type PortalTemplate = {
name: string
url: string
}