Prechádzať zdrojové kódy

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
Alexandre Bourdin 3 rokov pred
rodič
commit
7608d37c0a

+ 37 - 9
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,
   })
 }
 

+ 1 - 0
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

+ 1 - 0
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": "",

+ 18 - 0
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<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) => (

+ 111 - 48
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('<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)
+    })
+
+    afterEach(function () {
+      window.metaAttributesCache = new Map()
     })
 
-    render(<NewProjectButton id="test" />)
+    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')
 
-    const newProjectButton = screen.getByRole('button', {
-      name: 'New Project',
+      // static text
+      screen.getByText('Templates')
+
+      // dynamic menu based on templateLinks
+      screen.getByText('Academic Journal')
+      screen.getByText('View All')
     })
-    fireEvent.click(newProjectButton)
-  })
 
-  afterEach(function () {
-    window.metaAttributesCache = new Map()
-  })
+    it('open new project modal when clicking at Blank Project', function () {
+      fireEvent.click(screen.getByRole('menuitem', { name: 'Blank Project' }))
 
-  it('opens a dropdown', function () {
-    // static menu
-    screen.getByText('Blank Project')
-    screen.getByText('Example Project')
-    screen.getByText('Upload Project')
-    screen.getByText('Import from GitHub')
+      screen.getByPlaceholderText('Project Name')
+    })
 
-    // static text
-    screen.getByText('Templates')
+    it('open new project modal when clicking at Example Project', function () {
+      fireEvent.click(screen.getByRole('menuitem', { name: 'Example Project' }))
 
-    // dynamic menu based on templateLinks
-    screen.getByText('Academic Journal')
-    screen.getByText('View All')
-  })
+      screen.getByPlaceholderText('Project Name')
+    })
 
-  it('open new project modal when clicking at Blank Project', function () {
-    fireEvent.click(screen.getByRole('menuitem', { name: 'Blank Project' }))
+    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' }))
 
-    screen.getByPlaceholderText('Project Name')
-  })
+      expect(screen.queryByRole('dialog')).to.be.null
+    })
 
-  it('open new project modal when clicking at Example Project', function () {
-    fireEvent.click(screen.getByRole('menuitem', { name: 'Example Project' }))
+    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' }))
 
-    screen.getByPlaceholderText('Project Name')
+      expect(screen.queryByRole('dialog')).to.be.null
+    })
   })
 
-  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' }))
+  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',
+          },
+        ],
+      })
 
-    expect(screen.queryByRole('dialog')).to.be.null
-  })
+      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')
 
-  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' }))
+      // static text for institution templates
+      screen.getByText('Institution Templates')
 
-    expect(screen.queryByRole('dialog')).to.be.null
+      // 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')
+    })
   })
 })

+ 4 - 0
services/web/types/portal-template.ts

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