Просмотр исходного кода

Merge pull request #21971 from overleaf/td-ds-nav-split-test

Add split test for DS unified nav

GitOrigin-RevId: d649661568f9c1de9d2fe47f4491540e80830ab3
Tim Down 1 год назад
Родитель
Сommit
967c3e597c

+ 9 - 0
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,

+ 4 - 3
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

+ 20 - 0
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 (
+    <OLRow className="row-spaced">
+      <OLCol xs={{ span: 8, offset: 2 }} aria-live="polite">
+        <div className="notification-list">
+          <Notification
+            content={t('generic_something_went_wrong')}
+            type="error"
+          />
+        </div>
+      </OLCol>
+    </OLRow>
+  )
+}

+ 119 - 0
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 = (
+    <div className="pt-2 pb-3 d-md-none d-flex gap-2">
+      <NewProjectButton
+        id="new-project-button-projects-table"
+        showAddAffiliationWidget
+      />
+      <SearchForm
+        inputValue={searchText}
+        setInputValue={setSearchText}
+        filter={filter}
+        selectedTag={selectedTag}
+        className="overflow-hidden flex-grow-1"
+      />
+    </div>
+  )
+
+  return (
+    <>
+      <Sidebar />
+      <div className="project-list-main-react">
+        {error ? <DashApiError /> : ''}
+        <OLRow>
+          <OLCol>
+            <UserNotifications />
+          </OLCol>
+        </OLRow>
+        <div className="project-list-header-row">
+          <ProjectListTitle
+            filter={filter}
+            selectedTag={selectedTag}
+            selectedTagId={selectedTagId}
+            className="text-truncate d-none d-md-block"
+          />
+          <div className="project-tools">
+            <div className="d-none d-md-block">
+              {selectedProjects.length === 0 ? (
+                <CurrentPlanWidget />
+              ) : (
+                <ProjectTools />
+              )}
+            </div>
+            <div className="d-md-none">
+              <CurrentPlanWidget />
+            </div>
+          </div>
+        </div>
+        <OLRow className="d-none d-md-block">
+          <OLCol lg={7}>
+            <SearchForm
+              inputValue={searchText}
+              setInputValue={setSearchText}
+              filter={filter}
+              selectedTag={selectedTag}
+            />
+          </OLCol>
+        </OLRow>
+        <div className="project-list-sidebar-survey-wrapper d-md-none">
+          <SurveyWidget />
+        </div>
+        <div className="mt-1 d-md-none">
+          <div
+            role="toolbar"
+            className="projects-toolbar"
+            aria-label={t('projects')}
+          >
+            <ProjectsDropdown />
+            <SortByDropdown />
+          </div>
+        </div>
+        <OLRow className="row-spaced">
+          <OLCol>
+            <TableContainer bordered>
+              {tableTopArea}
+              <ProjectListTable />
+            </TableContainer>
+          </OLCol>
+        </OLRow>
+        <OLRow className="row-spaced">
+          <OLCol>
+            <LoadMore />
+          </OLCol>
+        </OLRow>
+      </div>
+    </>
+  )
+}

+ 77 - 164
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 DefaultNavbarAndFooter({ children }: { children: ReactNode }) {
+  const navbarProps = getMeta('ol-navbar')
+  const footerProps = getMeta('ol-footer')
+
+  return (
+    <>
+      <DefaultNavbar {...navbarProps} />
+      <main
+        id="main-content"
+        className="content content-alt project-list-react"
+      >
+        {children}
+      </main>
+      <FatFooter {...footerProps} />
+    </>
+  )
+}
+
+function DefaultPageContentWrapper({ children }: { children: ReactNode }) {
+  return (
+    <DefaultNavbarAndFooter>
+      <SystemMessages />
+      <div className="project-list-wrapper">{children}</div>
+    </DefaultNavbarAndFooter>
+  )
+}
+
 function ProjectListPageContent() {
-  const {
-    totalProjectsCount,
-    error,
-    isLoading,
-    loadProgress,
-    searchText,
-    setSearchText,
-    selectedProjects,
-    filter,
-    tags,
-    selectedTagId,
-  } = useProjectListContext()
-
-  const selectedTag = tags.find(tag => tag._id === selectedTagId)
+  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 = (
-    <div className="pt-2 pb-3 d-md-none">
-      <div className="clearfix">
-        <NewProjectButton
-          id="new-project-button-projects-table"
-          className="pull-left me-2"
-          showAddAffiliationWidget
-        />
-        <SearchForm
-          inputValue={searchText}
-          setInputValue={setSearchText}
-          filter={filter}
-          selectedTag={selectedTag}
-          className="overflow-hidden"
-        />
-      </div>
-    </div>
-  )
+  const hasDsNav =
+    splitTestVariants['sidebar-navigation-ui-update'] === 'active'
 
-  return isLoading ? (
-    <div className="loading-container">
+  if (isLoading) {
+    const loadingComponent = (
       <LoadingBranded loadProgress={loadProgress} label={t('loading')} />
-    </div>
-  ) : (
-    <>
-      <SystemMessages />
+    )
 
-      <div className="project-list-wrapper">
-        {totalProjectsCount > 0 ? (
-          <>
-            <Sidebar />
-            <div className="project-list-main-react">
-              {error ? <DashApiError /> : ''}
-              <OLRow>
-                <OLCol>
-                  <UserNotifications />
-                </OLCol>
-              </OLRow>
-              <div className="project-list-header-row">
-                <ProjectListTitle
-                  filter={filter}
-                  selectedTag={selectedTag}
-                  selectedTagId={selectedTagId}
-                  className="text-truncate d-none d-md-block"
-                />
-                <div className="project-tools">
-                  <div className="d-none d-md-block">
-                    {selectedProjects.length === 0 ? (
-                      <CurrentPlanWidget />
-                    ) : (
-                      <ProjectTools />
-                    )}
-                  </div>
-                  <div className="d-md-none">
-                    <CurrentPlanWidget />
-                  </div>
-                </div>
-              </div>
-              <OLRow className="d-none d-md-block">
-                <OLCol lg={7}>
-                  <SearchForm
-                    inputValue={searchText}
-                    setInputValue={setSearchText}
-                    filter={filter}
-                    selectedTag={selectedTag}
-                  />
-                </OLCol>
-              </OLRow>
-              <div className="project-list-sidebar-survey-wrapper d-md-none">
-                <SurveyWidget />
-              </div>
-              <div className="mt-1 d-md-none">
-                <div
-                  role="toolbar"
-                  className="projects-toolbar"
-                  aria-label={t('projects')}
-                >
-                  <ProjectsDropdown />
-                  <SortByDropdown />
-                </div>
-              </div>
-              <OLRow className="row-spaced">
-                <OLCol>
-                  <TableContainer bordered>
-                    {tableTopArea}
-                    <ProjectListTable />
-                  </TableContainer>
-                </OLCol>
-              </OLRow>
-              <OLRow className="row-spaced">
-                <OLCol>
-                  <LoadMore />
-                </OLCol>
-              </OLRow>
-            </div>
-          </>
-        ) : (
-          <div className="project-list-welcome-wrapper">
-            {error ? <DashApiError /> : ''}
-            <OLRow className="row-spaced mx-0">
-              <OLCol
-                md={{ span: 10, offset: 1 }}
-                lg={{ span: 8, offset: 2 }}
-                className="project-list-empty-col"
-              >
-                <OLRow>
-                  <OLCol>
-                    <UserNotifications />
-                  </OLCol>
-                </OLRow>
-                <WelcomeMessage />
-              </OLCol>
-            </OLRow>
-          </div>
-        )}
-      </div>
-    </>
-  )
-}
+    if (hasDsNav) {
+      return loadingComponent
+    } else {
+      return (
+        <DefaultNavbarAndFooter>
+          <div className="loading-container">{loadingComponent}</div>
+        </DefaultNavbarAndFooter>
+      )
+    }
+  }
 
-function DashApiError() {
-  const { t } = useTranslation()
-  return (
-    <OLRow className="row-spaced">
-      <OLCol xs={{ span: 8, offset: 2 }} aria-live="polite">
-        <div className="notification-list">
-          <Notification
-            content={t('generic_something_went_wrong')}
-            type="error"
-          />
-        </div>
-      </OLCol>
-    </OLRow>
-  )
+  if (totalProjectsCount === 0) {
+    return (
+      <DefaultPageContentWrapper>
+        <WelcomePageContent />
+      </DefaultPageContentWrapper>
+    )
+  } else if (hasDsNav) {
+    return (
+      <>
+        <div>Header with cut-down nav</div>
+        <div>Project list with DS nav and footer</div>
+      </>
+    )
+  } else {
+    return (
+      <DefaultPageContentWrapper>
+        <ProjectListDefault />
+      </DefaultPageContentWrapper>
+    )
+  }
 }
 
 export default withErrorBoundary(ProjectListRoot, GenericErrorBoundaryFallback)

+ 30 - 0
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 (
+    <div className="project-list-welcome-wrapper">
+      {error ? <DashApiError /> : ''}
+      <OLRow className="row-spaced mx-0">
+        <OLCol
+          md={{ span: 10, offset: 1 }}
+          lg={{ span: 8, offset: 2 }}
+          className="project-list-empty-col"
+        >
+          <OLRow>
+            <OLCol>
+              <UserNotifications />
+            </OLCol>
+          </OLRow>
+          <WelcomeMessage />
+        </OLCol>
+      </OLRow>
+    </div>
+  )
+}

+ 0 - 1
services/web/frontend/js/pages/project-list.jsx → 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'
 

+ 1 - 1
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});

+ 12 - 0
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('<NewProjectButton />', function () {
+  let isBootstrap5Stub: SinonStub
+
+  before(function () {
+    isBootstrap5Stub = sinon.stub(bootstrapUtils, 'isBootstrap5').returns(true)
+  })
+
+  after(function () {
+    isBootstrap5Stub.restore()
+  })
+
   beforeEach(function () {
     fetchMock.reset()
   })

+ 7 - 0
services/web/test/frontend/features/project-list/components/project-list-root.test.tsx

@@ -54,6 +54,13 @@ describe('<ProjectListRoot />', 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,

+ 16 - 0
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('<ProjectTools />', 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', [])
   })