mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-22 02:18:27 +00:00
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:
parent
68867d63b4
commit
967c3e597c
11 changed files with 295 additions and 169 deletions
|
@ -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,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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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'
|
||||
|
|
@ -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});
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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', [])
|
||||
})
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue