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

Add split test for DS unified nav

GitOrigin-RevId: d649661568f9c1de9d2fe47f4491540e80830ab3
This commit is contained in:
Tim Down 2024-11-27 14:12:50 +00:00 committed by Copybot
parent 68867d63b4
commit 967c3e597c
11 changed files with 295 additions and 169 deletions

View file

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

View file

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

View file

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

View file

@ -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>
</>
)
}

View file

@ -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 (
<>
<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, 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)

View file

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

View file

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

View file

@ -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});

View file

@ -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()
})

View file

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

View file

@ -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', [])
})