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 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,
})
}

View file

@ -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

View file

@ -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": "",

View file

@ -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<Nullable<NewProjectButtonModalVariant>>(null)
const portalTemplates = getMeta('ol-portalTemplates') as PortalTemplate[]
return (
<>
@ -48,6 +50,22 @@ function NewProjectButton({
<MenuItem onClick={() => setModal('import_from_github')}>
{t('import_from_github')}
</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 header>{t('templates')}</MenuItem>
{templateLinks.map((templateLink, index) => (

View file

@ -3,70 +3,133 @@ import { expect } from 'chai'
import NewProjectButton from '../../../../../frontend/js/features/project-list/components/new-project-button'
describe('<NewProjectButton />', 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(<NewProjectButton id="test" />)
const newProjectButton = screen.getByRole('button', {
name: 'New Project',
})
fireEvent.click(newProjectButton)
})
render(<NewProjectButton id="test" />)
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(<NewProjectButton id="test" />)
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')
})
})
})

View file

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