From 8d678b38409adbfe05cf5fda2523d49e810c7eee Mon Sep 17 00:00:00 2001
From: Tim Down <158919+timdown@users.noreply.github.com>
Date: Wed, 27 Nov 2024 14:12:50 +0000
Subject: [PATCH] Merge pull request #21971 from overleaf/td-ds-nav-split-test
Add split test for DS unified nav
GitOrigin-RevId: d649661568f9c1de9d2fe47f4491540e80830ab3
---
.../Project/ProjectListController.mjs | 9 +
services/web/app/views/project/list-react.pug | 7 +-
.../components/dash-api-error.tsx | 20 ++
.../components/project-list-default.tsx | 119 +++++++++
.../components/project-list-root.tsx | 241 ++++++------------
.../components/welcome-page-content.tsx | 30 +++
.../{project-list.jsx => project-list.tsx} | 1 -
.../bootstrap-5/pages/project-list.scss | 2 +-
.../components/new-project-button.test.tsx | 12 +
.../components/project-list-root.test.tsx | 7 +
.../project-tools-rename.test.tsx | 16 ++
11 files changed, 295 insertions(+), 169 deletions(-)
create mode 100644 services/web/frontend/js/features/project-list/components/dash-api-error.tsx
create mode 100644 services/web/frontend/js/features/project-list/components/project-list-default.tsx
create mode 100644 services/web/frontend/js/features/project-list/components/welcome-page-content.tsx
rename services/web/frontend/js/pages/{project-list.jsx => project-list.tsx} (92%)
diff --git a/services/web/app/src/Features/Project/ProjectListController.mjs b/services/web/app/src/Features/Project/ProjectListController.mjs
index e3e7f6cfe6..8c173e0f6d 100644
--- a/services/web/app/src/Features/Project/ProjectListController.mjs
+++ b/services/web/app/src/Features/Project/ProjectListController.mjs
@@ -443,6 +443,15 @@ async function projectListPage(req, res, next) {
)
}
+ // Get the user's assignment for the DS unified nav split test, which
+ // populates splitTestVariants with a value for the split test name and allows
+ // Pug to send it to the browser
+ await SplitTestHandler.promises.getAssignment(
+ req,
+ res,
+ 'sidebar-navigation-ui-update'
+ )
+
res.render('project/list-react', {
title: 'your_projects',
usersBestSubscription,
diff --git a/services/web/app/views/project/list-react.pug b/services/web/app/views/project/list-react.pug
index d7c9295e14..5b452118f1 100644
--- a/services/web/app/views/project/list-react.pug
+++ b/services/web/app/views/project/list-react.pug
@@ -4,7 +4,9 @@ block entrypointVar
- entrypoint = 'pages/project-list'
block vars
- - var suppressNavContentLinks = true
+ - const suppressNavContentLinks = true
+ - const suppressNavbar = true
+ - const suppressFooter = true
- bootstrap5PageStatus = 'enabled' // One of 'disabled', 'enabled', and 'queryStringOnly'
block append meta
@@ -40,5 +42,4 @@ block append meta
meta(name="ol-usGovBannerVariant" data-type="string" content=usGovBannerVariant)
block content
- main.content.content-alt.project-list-react#main-content
- #project-list-root
+ #project-list-root
diff --git a/services/web/frontend/js/features/project-list/components/dash-api-error.tsx b/services/web/frontend/js/features/project-list/components/dash-api-error.tsx
new file mode 100644
index 0000000000..da37b00baa
--- /dev/null
+++ b/services/web/frontend/js/features/project-list/components/dash-api-error.tsx
@@ -0,0 +1,20 @@
+import { useTranslation } from 'react-i18next'
+import OLRow from '@/features/ui/components/ol/ol-row'
+import OLCol from '@/features/ui/components/ol/ol-col'
+import Notification from '@/shared/components/notification'
+
+export default function DashApiError() {
+ const { t } = useTranslation()
+ return (
+
+
+
+
+
+
+
+ )
+}
diff --git a/services/web/frontend/js/features/project-list/components/project-list-default.tsx b/services/web/frontend/js/features/project-list/components/project-list-default.tsx
new file mode 100644
index 0000000000..307e11e490
--- /dev/null
+++ b/services/web/frontend/js/features/project-list/components/project-list-default.tsx
@@ -0,0 +1,119 @@
+import { useProjectListContext } from '../context/project-list-context'
+import { useTranslation } from 'react-i18next'
+import CurrentPlanWidget from './current-plan-widget/current-plan-widget'
+import NewProjectButton from './new-project-button'
+import ProjectListTable from './table/project-list-table'
+import SurveyWidget from './survey-widget'
+import UserNotifications from './notifications/user-notifications'
+import SearchForm from './search-form'
+import ProjectsDropdown from './dropdown/projects-dropdown'
+import SortByDropdown from './dropdown/sort-by-dropdown'
+import ProjectTools from './table/project-tools/project-tools'
+import ProjectListTitle from './title/project-list-title'
+import Sidebar from './sidebar/sidebar'
+import LoadMore from './load-more'
+import OLCol from '@/features/ui/components/ol/ol-col'
+import OLRow from '@/features/ui/components/ol/ol-row'
+import { TableContainer } from '@/features/ui/components/bootstrap-5/table'
+import DashApiError from '@/features/project-list/components/dash-api-error'
+
+export default function ProjectListDefault() {
+ const { t } = useTranslation()
+ const {
+ error,
+ searchText,
+ setSearchText,
+ selectedProjects,
+ filter,
+ tags,
+ selectedTagId,
+ } = useProjectListContext()
+
+ const selectedTag = tags.find(tag => tag._id === selectedTagId)
+
+ const tableTopArea = (
+
+
+
+
+ )
+
+ return (
+ <>
+
+
+ {error ?
: ''}
+
+
+
+
+
+
+
+
+
+ {selectedProjects.length === 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {tableTopArea}
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
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 7eb287c362..197662e03f 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
@@ -1,34 +1,25 @@
+import { ReactNode, useEffect } from 'react'
import {
ProjectListProvider,
useProjectListContext,
} from '../context/project-list-context'
+import {
+ SplitTestProvider,
+ useSplitTestContext,
+} from '@/shared/context/split-test-context'
import { ColorPickerProvider } from '../context/color-picker-context'
import * as eventTracking from '../../../infrastructure/event-tracking'
import { useTranslation } from 'react-i18next'
import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n'
-import CurrentPlanWidget from './current-plan-widget/current-plan-widget'
-import NewProjectButton from './new-project-button'
-import ProjectListTable from './table/project-list-table'
-import SurveyWidget from './survey-widget'
-import WelcomeMessage from './welcome-message'
import LoadingBranded from '../../../shared/components/loading-branded'
import SystemMessages from '../../../shared/components/system-messages'
-import UserNotifications from './notifications/user-notifications'
-import SearchForm from './search-form'
-import ProjectsDropdown from './dropdown/projects-dropdown'
-import SortByDropdown from './dropdown/sort-by-dropdown'
-import ProjectTools from './table/project-tools/project-tools'
-import ProjectListTitle from './title/project-list-title'
-import Sidebar from './sidebar/sidebar'
-import LoadMore from './load-more'
-import { useEffect } from 'react'
import withErrorBoundary from '../../../infrastructure/error-boundary'
-import { GenericErrorBoundaryFallback } from '../../../shared/components/generic-error-boundary-fallback'
-import { SplitTestProvider } from '@/shared/context/split-test-context'
-import OLCol from '@/features/ui/components/ol/ol-col'
-import Notification from '@/shared/components/notification'
-import OLRow from '@/features/ui/components/ol/ol-row'
-import { TableContainer } from '@/features/ui/components/bootstrap-5/table'
+import { GenericErrorBoundaryFallback } from '@/shared/components/generic-error-boundary-fallback'
+import getMeta from '@/utils/meta'
+import DefaultNavbar from '@/features/ui/components/bootstrap-5/navbar/default-navbar'
+import FatFooter from '@/features/ui/components/bootstrap-5/footer/fat-footer'
+import WelcomePageContent from '@/features/project-list/components/welcome-page-content'
+import ProjectListDefault from '@/features/project-list/components/project-list-default'
function ProjectListRoot() {
const { isReady } = useWaitForI18n()
@@ -52,21 +43,38 @@ export function ProjectListRootInner() {
)
}
-function ProjectListPageContent() {
- const {
- totalProjectsCount,
- error,
- isLoading,
- loadProgress,
- searchText,
- setSearchText,
- selectedProjects,
- filter,
- tags,
- selectedTagId,
- } = useProjectListContext()
+function DefaultNavbarAndFooter({ children }: { children: ReactNode }) {
+ const navbarProps = getMeta('ol-navbar')
+ const footerProps = getMeta('ol-footer')
- const selectedTag = tags.find(tag => tag._id === selectedTagId)
+ return (
+ <>
+
+
+ {children}
+
+
+ >
+ )
+}
+
+function DefaultPageContentWrapper({ children }: { children: ReactNode }) {
+ return (
+
+
+ {children}
+
+ )
+}
+
+function ProjectListPageContent() {
+ const { totalProjectsCount, isLoading, loadProgress } =
+ useProjectListContext()
+
+ const { splitTestVariants } = useSplitTestContext()
useEffect(() => {
eventTracking.sendMB('loads_v2_dash', {})
@@ -74,140 +82,45 @@ function ProjectListPageContent() {
const { t } = useTranslation()
- const tableTopArea = (
-
- )
+ const hasDsNav =
+ splitTestVariants['sidebar-navigation-ui-update'] === 'active'
- return isLoading ? (
-
+ if (isLoading) {
+ const loadingComponent = (
-
- ) : (
- <>
-
+ )
-
- {totalProjectsCount > 0 ? (
- <>
-
-
- {error ?
: ''}
-
-
-
-
-
-
-
-
-
- {selectedProjects.length === 0 ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {tableTopArea}
-
-
-
-
-
-
-
-
-
-
- >
- ) : (
-
- {error ?
: ''}
-
-
-
-
-
-
-
-
-
-
-
- )}
-
- >
- )
-}
+ if (hasDsNav) {
+ return loadingComponent
+ } else {
+ return (
+
+ {loadingComponent}
+
+ )
+ }
+ }
-function DashApiError() {
- const { t } = useTranslation()
- return (
-
-
-
-
-
-
-
- )
+ if (totalProjectsCount === 0) {
+ return (
+
+
+
+ )
+ } else if (hasDsNav) {
+ return (
+ <>
+ Header with cut-down nav
+ Project list with DS nav and footer
+ >
+ )
+ } else {
+ return (
+
+
+
+ )
+ }
}
export default withErrorBoundary(ProjectListRoot, GenericErrorBoundaryFallback)
diff --git a/services/web/frontend/js/features/project-list/components/welcome-page-content.tsx b/services/web/frontend/js/features/project-list/components/welcome-page-content.tsx
new file mode 100644
index 0000000000..083598c213
--- /dev/null
+++ b/services/web/frontend/js/features/project-list/components/welcome-page-content.tsx
@@ -0,0 +1,30 @@
+import { useProjectListContext } from '@/features/project-list/context/project-list-context'
+import DashApiError from '@/features/project-list/components/dash-api-error'
+import OLRow from '@/features/ui/components/ol/ol-row'
+import OLCol from '@/features/ui/components/ol/ol-col'
+import UserNotifications from '@/features/project-list/components/notifications/user-notifications'
+import WelcomeMessage from '@/features/project-list/components/welcome-message'
+
+export default function WelcomePageContent() {
+ const { error } = useProjectListContext()
+
+ return (
+
+ {error ?
: ''}
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/services/web/frontend/js/pages/project-list.jsx b/services/web/frontend/js/pages/project-list.tsx
similarity index 92%
rename from services/web/frontend/js/pages/project-list.jsx
rename to services/web/frontend/js/pages/project-list.tsx
index 68f4564c46..3886db545e 100644
--- a/services/web/frontend/js/pages/project-list.jsx
+++ b/services/web/frontend/js/pages/project-list.tsx
@@ -5,7 +5,6 @@ import './../i18n'
import '../features/event-tracking'
import '../features/cookie-banner'
import '../features/link-helpers/slow-link'
-import '../features/header-footer-react'
import ReactDOM from 'react-dom'
import ProjectListRoot from '../features/project-list/components/project-list-root'
diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss
index ca949ef486..fcc372d0b5 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/pages/project-list.scss
@@ -18,7 +18,7 @@
}
.project-list-react {
- body > &.content {
+ #project-list-root > &.content {
padding-top: $header-height;
padding-bottom: 0;
min-height: calc(100vh - #{$header-height});
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 93c7a68e3a..d3c465434e 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
@@ -4,8 +4,20 @@ import fetchMock from 'fetch-mock'
import NewProjectButton from '../../../../../frontend/js/features/project-list/components/new-project-button'
import { renderWithProjectListContext } from '../helpers/render-with-context'
import getMeta from '@/utils/meta'
+import * as bootstrapUtils from '@/features/utils/bootstrap-5'
+import sinon, { type SinonStub } from 'sinon'
describe('', function () {
+ let isBootstrap5Stub: SinonStub
+
+ before(function () {
+ isBootstrap5Stub = sinon.stub(bootstrapUtils, 'isBootstrap5').returns(true)
+ })
+
+ after(function () {
+ isBootstrap5Stub.restore()
+ })
+
beforeEach(function () {
fetchMock.reset()
})
diff --git a/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx b/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx
index 7139aa61c3..b01e86b426 100644
--- a/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx
+++ b/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx
@@ -54,6 +54,13 @@ describe('', function () {
// we need a blank user here since its used in checking if we should display certain ads
window.metaAttributesCache.set('ol-user', {})
window.metaAttributesCache.set('ol-user_id', userId)
+ window.metaAttributesCache.set('ol-footer', {
+ translatedLanguages: { en: 'English' },
+ subdomainLang: { en: { lngCode: 'en', url: 'overleaf.com' } },
+ })
+ window.metaAttributesCache.set('ol-navbar', {
+ items: [],
+ })
assignStub = sinon.stub()
this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({
assign: assignStub,
diff --git a/services/web/test/frontend/features/project-list/components/table/project-tools/project-tools-rename.test.tsx b/services/web/test/frontend/features/project-list/components/table/project-tools/project-tools-rename.test.tsx
index 276c98fdee..44bc7e0fed 100644
--- a/services/web/test/frontend/features/project-list/components/table/project-tools/project-tools-rename.test.tsx
+++ b/services/web/test/frontend/features/project-list/components/table/project-tools/project-tools-rename.test.tsx
@@ -4,6 +4,8 @@ import moment from 'moment/moment'
import fetchMock from 'fetch-mock'
import { Project } from '../../../../../../../types/project/dashboard/api'
import { ProjectListRootInner } from '@/features/project-list/components/project-list-root'
+import * as bootstrapUtils from '@/features/utils/bootstrap-5'
+import sinon, { type SinonStub } from 'sinon'
const users = {
picard: {
@@ -46,12 +48,26 @@ const projects: Project[] = [
]
describe('', function () {
+ let isBootstrap5Stub: SinonStub
+
+ before(function () {
+ isBootstrap5Stub = sinon.stub(bootstrapUtils, 'isBootstrap5').returns(true)
+ })
+
+ after(function () {
+ isBootstrap5Stub.restore()
+ })
beforeEach(function () {
window.metaAttributesCache.set('ol-user', {})
window.metaAttributesCache.set('ol-prefetchedProjectsBlob', {
projects,
totalSize: 100,
})
+
+ window.metaAttributesCache.set('ol-footer', {
+ translatedLanguages: { en: 'English' },
+ subdomainLang: { en: { lngCode: 'en', url: 'overleaf.com' } },
+ })
fetchMock.get('/system/messages', [])
})