diff --git a/services/web/app/src/Features/Project/ProjectListController.js b/services/web/app/src/Features/Project/ProjectListController.js index b24df18cd1..9e0a05ee14 100644 --- a/services/web/app/src/Features/Project/ProjectListController.js +++ b/services/web/app/src/Features/Project/ProjectListController.js @@ -293,6 +293,7 @@ async function projectListReactPage(req, res, next) { notifications, notificationsInstitution, user, + userAffiliations, userEmails, reconfirmedViaSAML, allInReconfirmNotificationPeriods, diff --git a/services/web/app/views/project/list-react.pug b/services/web/app/views/project/list-react.pug index b52e2839fc..cac3ebd9ac 100644 --- a/services/web/app/views/project/list-react.pug +++ b/services/web/app/views/project/list-react.pug @@ -13,6 +13,7 @@ block append meta meta(name="ol-userEmails" data-type="json" content=userEmails) meta(name="ol-allInReconfirmNotificationPeriods" data-type="json" content=allInReconfirmNotificationPeriods) meta(name="ol-user" data-type="json" content=user) + meta(name="ol-userAffiliations" data-type="json" content=userAffiliations) meta(name="ol-reconfirmedViaSAML" content=reconfirmedViaSAML) meta(name="ol-survey" data-type="json" content=survey) meta(name="ol-tags" data-type="json" content=tags) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 3cc1ba1ffe..300eb19157 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -30,6 +30,7 @@ "archived": "", "archived_projects": "", "archiving_projects_wont_affect_collaborators": "", + "are_you_affiliated_with_an_institution": "", "are_you_still_at": "", "ascending": "", "ask_proj_owner_to_upgrade_for_git_bridge": "", diff --git a/services/web/frontend/js/features/project-list/components/project-list-root.tsx b/services/web/frontend/js/features/project-list/components/project-list-root.tsx index 1bac43455c..1205aecd7c 100644 --- a/services/web/frontend/js/features/project-list/components/project-list-root.tsx +++ b/services/web/frontend/js/features/project-list/components/project-list-root.tsx @@ -10,6 +10,7 @@ import CurrentPlanWidget from './current-plan-widget/current-plan-widget' import NewProjectButton from './new-project-button' import ProjectListTable from './table/project-list-table' import SidebarFilters from './sidebar/sidebar-filters' +import AddAffiliation, { useAddAffiliation } from './sidebar/add-affiliation' import SurveyWidget from './survey-widget' import WelcomeMessage from './welcome-message' import LoadingBranded from '../../../shared/components/loading-branded' @@ -42,6 +43,7 @@ function ProjectListPageContent() { setSearchText, selectedProjects, } = useProjectListContext() + const { show: showAddAffiliationWidget } = useAddAffiliation() useEffect(() => { eventTracking.sendMB('loads_v2_dash', {}) @@ -62,6 +64,8 @@ function ProjectListPageContent() { diff --git a/services/web/frontend/js/features/project-list/components/sidebar/add-affiliation.tsx b/services/web/frontend/js/features/project-list/components/sidebar/add-affiliation.tsx new file mode 100644 index 0000000000..057aae94b2 --- /dev/null +++ b/services/web/frontend/js/features/project-list/components/sidebar/add-affiliation.tsx @@ -0,0 +1,34 @@ +import { useTranslation } from 'react-i18next' +import { Button } from 'react-bootstrap' +import { useProjectListContext } from '../../context/project-list-context' +import getMeta from '../../../../utils/meta' +import { Affiliation } from '../../../../../../types/affiliation' +import { ExposedSettings } from '../../../../../../types/exposed-settings' + +export function useAddAffiliation() { + const { totalProjectsCount } = useProjectListContext() + const { isOverleaf } = getMeta('ol-ExposedSettings') as ExposedSettings + const userAffiliations = getMeta('ol-userAffiliations', []) as Affiliation[] + + return { show: isOverleaf && totalProjectsCount && !userAffiliations.length } +} + +function AddAffiliation() { + const { t } = useTranslation() + const { show } = useAddAffiliation() + + if (!show) { + return null + } + + return ( +
+

{t('are_you_affiliated_with_an_institution')}

+ +
+ ) +} + +export default AddAffiliation diff --git a/services/web/frontend/stories/project-list/add-affiliation.stories.tsx b/services/web/frontend/stories/project-list/add-affiliation.stories.tsx new file mode 100644 index 0000000000..51a1a16f43 --- /dev/null +++ b/services/web/frontend/stories/project-list/add-affiliation.stories.tsx @@ -0,0 +1,29 @@ +import AddAffiliation from '../../js/features/project-list/components/sidebar/add-affiliation' +import { ProjectListProvider } from '../../js/features/project-list/context/project-list-context' +import useFetchMock from '../hooks/use-fetch-mock' +import { projectsData } from '../../../test/frontend/features/project-list/fixtures/projects-data' + +export const Add = (args: any) => { + window.metaAttributesCache = new Map() + window.metaAttributesCache.set('ol-ExposedSettings', { + isOverleaf: true, + }) + window.metaAttributesCache.set('ol-userAffiliations', []) + useFetchMock(fetchMock => { + fetchMock.post(/\/api\/project/, { + projects: projectsData, + totalSize: projectsData.length, + }) + }) + + return ( + + + + ) +} + +export default { + title: 'Project List / Affiliation', + component: AddAffiliation, +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 4dd3232035..d9f0deb54f 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1584,6 +1584,7 @@ "link_institutional_email_get_started": "Link an institutional email address to your account to get started.", "looks_like_youre_at": "It looks like you’re at <0>__institutionName__!", "add_affiliation": "Add Affiliation", + "are_you_affiliated_with_an_institution": "Are you affiliated with an institution?", "did_you_know_institution_providing_professional": "Did you know that __institutionName__ is providing <0>free __appName__ Professional features to everyone at __institutionName__?", "add_email_to_claim_features": "Add an institutional email address to claim your features.", "please_change_primary_to_remove": "Please change your primary email in order to remove", diff --git a/services/web/test/frontend/features/project-list/components/sidebar/add-affiliation.test.tsx b/services/web/test/frontend/features/project-list/components/sidebar/add-affiliation.test.tsx new file mode 100644 index 0000000000..d0df0f0ae2 --- /dev/null +++ b/services/web/test/frontend/features/project-list/components/sidebar/add-affiliation.test.tsx @@ -0,0 +1,76 @@ +import { screen, waitFor } from '@testing-library/react' +import { expect } from 'chai' +import fetchMock from 'fetch-mock' +import { renderWithProjectListContext } from '../../helpers/render-with-context' +import AddAffiliation from '../../../../../../frontend/js/features/project-list/components/sidebar/add-affiliation' +import { Affiliation } from '../../../../../../types/affiliation' + +describe('Add affiliation widget', function () { + const validateNonExistence = () => { + expect(screen.queryByText(/are you affiliated with an institution/i)).to.be + .null + expect(screen.queryByRole('link', { name: /add affiliation/i })).to.be.null + } + + beforeEach(function () { + window.metaAttributesCache = new Map() + fetchMock.reset() + }) + + afterEach(function () { + window.metaAttributesCache = new Map() + fetchMock.reset() + }) + + it('renders the component', async function () { + window.metaAttributesCache.set('ol-ExposedSettings', { isOverleaf: true }) + window.metaAttributesCache.set('ol-userAffiliations', []) + + renderWithProjectListContext() + + await fetchMock.flush(true) + await waitFor(() => expect(fetchMock.called('/api/project'))) + + screen.getByText(/are you affiliated with an institution/i) + const addAffiliationLink = screen.getByRole('link', { + name: /add affiliation/i, + }) + expect(addAffiliationLink.getAttribute('href')).to.equal('/user/settings') + }) + + it('does not render when `isOverleaf` is `false`', async function () { + window.metaAttributesCache.set('ol-ExposedSettings', { isOverleaf: false }) + window.metaAttributesCache.set('ol-userAffiliations', []) + + renderWithProjectListContext() + + await fetchMock.flush(true) + await waitFor(() => expect(fetchMock.called('/api/project'))) + + validateNonExistence() + }) + + it('does not render when there no projects', async function () { + window.metaAttributesCache.set('ol-ExposedSettings', { isOverleaf: true }) + window.metaAttributesCache.set('ol-userAffiliations', []) + + renderWithProjectListContext(, { projects: [] }) + + await fetchMock.flush(true) + await waitFor(() => expect(fetchMock.called('/api/project'))) + + validateNonExistence() + }) + + it('does not render when there are affiliations', async function () { + window.metaAttributesCache.set('ol-ExposedSettings', { isOverleaf: true }) + window.metaAttributesCache.set('ol-userAffiliations', [{} as Affiliation]) + + renderWithProjectListContext() + + await fetchMock.flush(true) + await waitFor(() => expect(fetchMock.called('/api/project'))) + + validateNonExistence() + }) +})