mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Add DS nav page switcher behind overleaf-library flag (#33112)
* Add DS nav page switcher behind overleaf-library flag - Add shared DsNavPageSwitcher component (Library/Projects nav links + logo) - Show page switcher in projects sidebar when overleaf-library flag enabled - Hide 'All projects' filter and sidebar New Project button behind flag - Move New Project button to content area header when flag enabled - Prevent full page reload when clicking active nav item - Change Upgrade button to premium variant when flag enabled - Add overleaf-library split test to ProjectListController - Add library-page class to remove rounded corner on /library - Add Cypress component tests for DsNavPageSwitcher Closes #33092 GitOrigin-RevId: 2e348da8307bf944d481b54b3a2bcc2eb319e18e
This commit is contained in:
@@ -530,6 +530,7 @@ async function projectListPage(req, res, next) {
|
||||
const splitTests = [
|
||||
// Split tests that will be made available to the frontend
|
||||
'import-docx',
|
||||
'overleaf-library',
|
||||
].filter(Boolean)
|
||||
|
||||
await Promise.all(
|
||||
|
||||
@@ -4,10 +4,12 @@ import OLTooltip from '@/shared/components/ol/ol-tooltip'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { FreePlanSubscription } from '../../../../../../types/project/dashboard/subscription'
|
||||
import * as eventTracking from '../../../../infrastructure/event-tracking'
|
||||
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||
|
||||
type FreePlanProps = Pick<FreePlanSubscription, 'featuresPageURL'>
|
||||
|
||||
function FreePlan({ featuresPageURL }: FreePlanProps) {
|
||||
const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
|
||||
const { t } = useTranslation()
|
||||
const currentPlanLabel = (
|
||||
<Trans
|
||||
@@ -43,7 +45,8 @@ function FreePlan({ featuresPageURL }: FreePlanProps) {
|
||||
</OLTooltip>{' '}
|
||||
<span className="d-none d-md-inline-block">
|
||||
<OLButton
|
||||
variant="primary"
|
||||
variant={isLibraryEnabled ? 'premium' : 'primary'}
|
||||
size={isLibraryEnabled ? 'sm' : undefined}
|
||||
href="/user/subscription/plans"
|
||||
onClick={handleClick}
|
||||
>
|
||||
|
||||
@@ -23,6 +23,7 @@ import overleafLogo from '@/shared/svgs/overleaf-a-ds-solution-mallard.svg'
|
||||
import overleafLogoDark from '@/shared/svgs/overleaf-a-ds-solution-mallard-dark.svg'
|
||||
import CookieBanner from '@/shared/components/cookie-banner'
|
||||
import { useActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme'
|
||||
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||
|
||||
export function ProjectListDsNav() {
|
||||
const navbarProps = getMeta('ol-navbar')
|
||||
@@ -38,27 +39,48 @@ export function ProjectListDsNav() {
|
||||
selectedTagId,
|
||||
} = useProjectListContext()
|
||||
const activeOverallTheme = useActiveOverallTheme()
|
||||
const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
|
||||
|
||||
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"
|
||||
/>
|
||||
{isLibraryEnabled ? (
|
||||
<>
|
||||
<SearchForm
|
||||
inputValue={searchText}
|
||||
setInputValue={setSearchText}
|
||||
filter={filter}
|
||||
selectedTag={selectedTag}
|
||||
className="overflow-hidden flex-grow-1"
|
||||
/>
|
||||
<NewProjectButton
|
||||
id="new-project-button-projects-table"
|
||||
showAddAffiliationWidget
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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 (
|
||||
<div className="project-ds-nav-page website-redesign">
|
||||
<div
|
||||
className={`project-ds-nav-page website-redesign${isLibraryEnabled ? ' library-enabled' : ''}`}
|
||||
>
|
||||
<SystemMessages />
|
||||
<DefaultNavbar
|
||||
{...navbarProps}
|
||||
@@ -96,8 +118,8 @@ export function ProjectListDsNav() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="project-ds-nav-project-list">
|
||||
<OLRow className="d-none d-md-block">
|
||||
<OLCol lg={7}>
|
||||
<OLRow className="d-none d-md-flex align-items-center">
|
||||
<OLCol md={isLibraryEnabled ? 8 : undefined} lg={7}>
|
||||
<SearchForm
|
||||
inputValue={searchText}
|
||||
setInputValue={setSearchText}
|
||||
@@ -105,6 +127,14 @@ export function ProjectListDsNav() {
|
||||
selectedTag={selectedTag}
|
||||
/>
|
||||
</OLCol>
|
||||
{isLibraryEnabled && (
|
||||
<OLCol className="ms-auto" xs="auto">
|
||||
<NewProjectButton
|
||||
id="new-project-button-projects-table"
|
||||
showAddAffiliationWidget
|
||||
/>
|
||||
</OLCol>
|
||||
)}
|
||||
</OLRow>
|
||||
<div className="project-list-sidebar-survey-wrapper d-md-none">
|
||||
{/* Omit the survey card in mobile view for now */}
|
||||
|
||||
@@ -7,10 +7,15 @@ import { usePersistedResize } from '@/shared/hooks/use-resize'
|
||||
import { useScrolled } from '@/features/project-list/components/sidebar/use-scroll'
|
||||
import { SurveyWidgetDsNav } from '@/features/project-list/components/survey-widget-ds-nav'
|
||||
import { SidebarLowerSection } from '@/shared/components/sidebar/sidebar-lower-section'
|
||||
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||
import { DsNavPageSwitcher } from '@/shared/components/sidebar/ds-nav-page-switcher'
|
||||
import { useProjectListContext } from '@/features/project-list/context/project-list-context'
|
||||
|
||||
function SidebarDsNav() {
|
||||
const { t } = useTranslation()
|
||||
const { show: showAddAffiliationWidget } = useAddAffiliation()
|
||||
const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
|
||||
const { selectFilter } = useProjectListContext()
|
||||
const { mousePos, getHandleProps, getTargetProps } = usePersistedResize({
|
||||
name: 'project-sidebar',
|
||||
})
|
||||
@@ -25,14 +30,26 @@ function SidebarDsNav() {
|
||||
},
|
||||
})}
|
||||
>
|
||||
{isLibraryEnabled && (
|
||||
<>
|
||||
<DsNavPageSwitcher
|
||||
activePage="projects"
|
||||
showLogo={false}
|
||||
onProjectsClick={() => selectFilter('all')}
|
||||
/>
|
||||
<hr className="ds-nav-page-switcher-divider" />
|
||||
</>
|
||||
)}
|
||||
<nav
|
||||
className="flex-grow flex-shrink"
|
||||
aria-label={t('project_categories_tags')}
|
||||
>
|
||||
<NewProjectButton
|
||||
id="new-project-button-sidebar"
|
||||
className={scrolledDown ? 'show-shadow' : undefined}
|
||||
/>
|
||||
{!isLibraryEnabled && (
|
||||
<NewProjectButton
|
||||
id="new-project-button-sidebar"
|
||||
className={scrolledDown ? 'show-shadow' : undefined}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="project-list-sidebar-scroll"
|
||||
ref={containerRef}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from '../../context/project-list-context'
|
||||
import TagsList from './tags-list'
|
||||
import ProjectsFilterMenu from '../projects-filter-menu'
|
||||
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||
|
||||
type SidebarFilterProps = {
|
||||
filter: Filter
|
||||
@@ -29,10 +30,13 @@ export function SidebarFilter({ filter, text }: SidebarFilterProps) {
|
||||
|
||||
export default function SidebarFilters() {
|
||||
const { t } = useTranslation()
|
||||
const isLibraryEnabled = isSplitTestEnabled('overleaf-library')
|
||||
|
||||
return (
|
||||
<ul className="list-unstyled project-list-filters">
|
||||
<SidebarFilter filter="all" text={t('all_projects')} />
|
||||
{!isLibraryEnabled && (
|
||||
<SidebarFilter filter="all" text={t('all_projects')} />
|
||||
)}
|
||||
<SidebarFilter filter="owned" text={t('your_projects')} />
|
||||
<SidebarFilter filter="shared" text={t('shared_with_you')} />
|
||||
<SidebarFilter filter="archived" text={t('archived_projects')} />
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BookBookmark, Folder } from '@phosphor-icons/react'
|
||||
import { useActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme'
|
||||
import overleafLogo from '@/shared/svgs/overleaf-a-ds-solution-mallard.svg'
|
||||
import overleafLogoDark from '@/shared/svgs/overleaf-a-ds-solution-mallard-dark.svg'
|
||||
|
||||
type ActivePage = 'library' | 'projects'
|
||||
|
||||
export function DsNavPageSwitcher({
|
||||
activePage,
|
||||
showLogo = true,
|
||||
onLibraryClick,
|
||||
onProjectsClick,
|
||||
}: {
|
||||
activePage: ActivePage
|
||||
showLogo?: boolean
|
||||
onLibraryClick?: React.MouseEventHandler
|
||||
onProjectsClick?: React.MouseEventHandler
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const activeOverallTheme = useActiveOverallTheme()
|
||||
|
||||
return (
|
||||
<>
|
||||
{showLogo && (
|
||||
<div className="ds-nav-page-switcher-logo">
|
||||
<img
|
||||
src={
|
||||
activeOverallTheme === 'dark' ? overleafLogoDark : overleafLogo
|
||||
}
|
||||
alt="Overleaf, A Digital Science Solution"
|
||||
height="59"
|
||||
width="130"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ul
|
||||
className={`list-unstyled ds-nav-page-switcher-items${!showLogo ? ' ds-nav-page-switcher-items--no-logo' : ''}`}
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="/library"
|
||||
className={`ds-nav-page-switcher-item${activePage === 'library' ? ' active' : ''}`}
|
||||
aria-current={activePage === 'library' ? 'page' : undefined}
|
||||
onClick={
|
||||
onLibraryClick
|
||||
? e => {
|
||||
e.preventDefault()
|
||||
onLibraryClick(e)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<BookBookmark size={24} />
|
||||
<span>{t('library')}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/project"
|
||||
className={`ds-nav-page-switcher-item${activePage === 'projects' ? ' active' : ''}`}
|
||||
aria-current={activePage === 'projects' ? 'page' : undefined}
|
||||
onClick={
|
||||
onProjectsClick
|
||||
? e => {
|
||||
e.preventDefault()
|
||||
onProjectsClick(e)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Folder size={24} />
|
||||
<span>{t('projects')}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -103,6 +103,36 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
&.library-enabled {
|
||||
.navbar-default .navbar-header .navbar-logo {
|
||||
@include media-breakpoint-up(md) {
|
||||
top: calc(var(--spacing-05) * 2);
|
||||
left: var(--spacing-05);
|
||||
}
|
||||
}
|
||||
|
||||
.project-list-wrapper .project-list-sidebar-wrapper-react {
|
||||
.ds-nav-page-switcher-divider {
|
||||
margin: var(--spacing-05) 0;
|
||||
}
|
||||
|
||||
.project-list-sidebar-scroll {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.library-page {
|
||||
.project-list-wrapper
|
||||
.project-ds-nav-content-and-messages
|
||||
.project-ds-nav-content {
|
||||
@include media-breakpoint-up(md) {
|
||||
border-top-left-radius: 0;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-nav > li > .dropdown-toggle::after {
|
||||
display: none;
|
||||
}
|
||||
@@ -156,6 +186,50 @@ body {
|
||||
var(--spacing-05);
|
||||
}
|
||||
|
||||
.ds-nav-page-switcher-logo {
|
||||
padding: 0 var(--spacing-08) var(--spacing-07);
|
||||
|
||||
img {
|
||||
width: 130px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.ds-nav-page-switcher-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-02);
|
||||
padding: 0 var(--spacing-05);
|
||||
margin: 0;
|
||||
|
||||
&--no-logo {
|
||||
padding-top: var(--spacing-05);
|
||||
}
|
||||
}
|
||||
|
||||
.ds-nav-page-switcher-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-05);
|
||||
padding: var(--spacing-05);
|
||||
border-radius: var(--border-radius-medium);
|
||||
color: var(--ds-nav-color);
|
||||
font-size: var(--font-size-03);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--ds-nav-hover-bg);
|
||||
color: var(--ds-nav-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--ds-nav-active-bg);
|
||||
color: var(--ds-nav-active-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.ds-nav-sidebar-lower {
|
||||
padding: var(--spacing-05) var(--spacing-08) 0 var(--spacing-05);
|
||||
border-top: solid 1px transparent;
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { DsNavPageSwitcher } from '../../../../../frontend/js/shared/components/sidebar/ds-nav-page-switcher'
|
||||
import { SplitTestProvider } from '@/shared/context/split-test-context'
|
||||
import { UserSettingsProvider } from '@/shared/context/user-settings-context'
|
||||
|
||||
function mountDsNavPageSwitcher(
|
||||
props: React.ComponentProps<typeof DsNavPageSwitcher>
|
||||
) {
|
||||
cy.mount(
|
||||
<SplitTestProvider>
|
||||
<UserSettingsProvider>
|
||||
<DsNavPageSwitcher {...props} />
|
||||
</UserSettingsProvider>
|
||||
</SplitTestProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('<DsNavPageSwitcher />', function () {
|
||||
describe('activePage="library"', function () {
|
||||
beforeEach(function () {
|
||||
mountDsNavPageSwitcher({ activePage: 'library' })
|
||||
})
|
||||
|
||||
it('marks the Library link as active', function () {
|
||||
cy.findByRole('link', { name: /library/i }).should('have.class', 'active')
|
||||
})
|
||||
|
||||
it('does not mark the Projects link as active', function () {
|
||||
cy.findByRole('link', { name: /projects/i }).should(
|
||||
'not.have.class',
|
||||
'active'
|
||||
)
|
||||
})
|
||||
|
||||
it('sets aria-current on the Library link', function () {
|
||||
cy.findByRole('link', { name: /library/i }).should(
|
||||
'have.attr',
|
||||
'aria-current',
|
||||
'page'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not set aria-current on the Projects link', function () {
|
||||
cy.findByRole('link', { name: /projects/i }).should(
|
||||
'not.have.attr',
|
||||
'aria-current'
|
||||
)
|
||||
})
|
||||
|
||||
it('renders the logo', function () {
|
||||
cy.get('.ds-nav-page-switcher-logo img').should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('activePage="projects"', function () {
|
||||
beforeEach(function () {
|
||||
mountDsNavPageSwitcher({ activePage: 'projects' })
|
||||
})
|
||||
|
||||
it('marks the Projects link as active', function () {
|
||||
cy.findByRole('link', { name: /projects/i }).should(
|
||||
'have.class',
|
||||
'active'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not mark the Library link as active', function () {
|
||||
cy.findByRole('link', { name: /library/i }).should(
|
||||
'not.have.class',
|
||||
'active'
|
||||
)
|
||||
})
|
||||
|
||||
it('renders the logo', function () {
|
||||
cy.get('.ds-nav-page-switcher-logo img').should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('link hrefs', function () {
|
||||
beforeEach(function () {
|
||||
mountDsNavPageSwitcher({ activePage: 'library' })
|
||||
})
|
||||
|
||||
it('Library link points to /library', function () {
|
||||
cy.findByRole('link', { name: /library/i }).should(
|
||||
'have.attr',
|
||||
'href',
|
||||
'/library'
|
||||
)
|
||||
})
|
||||
|
||||
it('Projects link points to /project', function () {
|
||||
cy.findByRole('link', { name: /projects/i }).should(
|
||||
'have.attr',
|
||||
'href',
|
||||
'/project'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('showLogo={false}', function () {
|
||||
beforeEach(function () {
|
||||
mountDsNavPageSwitcher({ activePage: 'projects', showLogo: false })
|
||||
})
|
||||
|
||||
it('does not render the logo', function () {
|
||||
cy.get('.ds-nav-page-switcher-logo').should('not.exist')
|
||||
})
|
||||
|
||||
it('still renders both nav links', function () {
|
||||
cy.findByRole('link', { name: /library/i }).should('exist')
|
||||
cy.findByRole('link', { name: /projects/i }).should('exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user